Lafine Systems Conception · Document technique

Livre blanc sur l'architecture et la sécurité de RoamSwitch

Ce document explique avec quels privilèges RoamSwitch s'exécute et ce qu'il fait à cette limite. Il ne contient aucun langage marketing ; tout ce qui est indiqué ici peut être vérifié par rapport à l'application d'expédition binaire et son comportement réel.

Version v1.6 Champ d'application RoamSwitch 1.11.14 (build 161) Prérequis macOS 13.0+ / Apple Silicon Publié 2026-09-23 Team ID GV76B6G4YU

§1

RoamSwitch est une application de barre de menus pour Mac. En fonction de votre confiance dans le réseau que vous faites actuellement connecté, il bascule automatiquement le pare-feu macOS, les services de partage, AirDrop, et DNS. It also watches for ARP spoofing, ports open to the outside, USB storage, and ransomware-like encryption activity, and when it detects something dangerous it will go as far as cutting off traffic with the packet filter (pf) — an emergency air-gap.

En d'autres termes, RoamSwitch installe un démon bénéficiant des privilèges root et, s'il le voulait, il pourrait tout arrêter. trafic réseau sur Mac. It is built by one person, and "please trust me" is not enough to justifier ce privilège. Au lieu de cela, ce document explique la conception sous une forme que vous pouvez vérifier.

Pour qui c'est

  • Les ingénieurs décident de l'installer ou non
  • Security researchers and journalists who want to understand the internals before a review or an article
  • Personnel de sécurité des entreprises partenaires évaluant un déploiement interne ou un regroupement OEM

Ce que ce document ne couvre pas

It does not go into detection-threshold tuning, false-positive statistics, or UI walkthroughs. Ce qu'il explique, ce sont quatre choses : les privilèges , les limites des processus, le flux de données et cryptographie. Les spécifications des fonctionnalités elles-mêmes se trouvent dans la ressource MCP fournie roamswitch://docs/features et dans l'aide intégrée à l'application.

Ce qui est divulgué et dans quelle mesure (politique de divulgation)

This document is written on the assumption that an attacker already has the distributed binary. Every endpoint URL, identifier, file path, XPC protocol, and embedded public key that appears here peut être retiré du colis d'expédition RoamSwitch.app en quelques minutes avec strings, codesign -d ou un proxy de trafic. Les écrire ici donc ne donne rien de nouveau à un attaquant. La seule chose qu'il fait progresser est la compréhension du critique.

On the other hand, server-side implementation that cannot be seen from the binary — rate-limit thresholds, keys, admin endpoints, the DB schema, the Firebase project layout — is not included. La conception de l'intégration des OEM et des partenaires est également hors de portée ici et est couverte dans un document distinct. document interne. « Ce que vous pouvez apprendre en observant le client » est la ligne de divulgation pour cela. documenter.

Complément

Ce document correspond à la source de la version notée en haut. Où les comportements changent in a later version, the document is revised and the version number and target build are updated. If you find a discrepancy between the text and the code, please let us know at lafine.net/contact.html.

§2

RoamSwitch.app est composé de trois exécutables. Un seul d’entre eux est privilégié ; le deux autres fonctionnent avec des droits d'utilisateur de connexion. All three ship with Hardened Runtime enabled, Developer ID signé et notarié.

Schéma des composants RoamSwitch et des frontières de confiance Client MCP Claude Desktop / Code MCPServer Privilège utilisateur Lecture seule RoamSwitch.app Privilège utilisateur Barre de menu / Surveillance / UI Helper root / LaunchDaemon SMAppService.daemon Binaires système pfctl / socketfilterfw … stdio (JSON-RPC) Lecture seule * XPC mach service Autorisé par signature de code Exécution du processus
* MCPServer ne se connecte pas à l'application elle-même ; it reads the shared preferences domain and the monitoring modules directly (§8).
Privilèges et rôle de chaque composant
ExécutablePrivilègePeut faireJe ne peux pas faire
RoamSwitch
.app
Utilisateur connecté Surveillez l'état du réseau, exécutez des diagnostics, dessinez l'interface utilisateur, appelez l'assistant via XPC, modifiez AirDrop via defaults, lancez ClamAV (facultatif) Faire fonctionner directement le pare-feu, pf ou les démons système (tout cela passe par l'assistant)
RoamSwitch
Helper
racine Uniquement les opérations répertoriées dans HelperProtocol (tableau §3) : pare-feu/furtivité, chargement/déchargement des démons de partage, application du jeu de règles pf, changement de DNS, envoi de signaux aux processus Tout le reste. Il n'existe aucune interface pour exécuter des commandes arbitraires. Aucun entitlement d'envoi réseau n'est déclaré, mais l'exécutable n'étant pas sandboxé, cela n'interdit pas en soi les communications (mesure du trafic sortant au §7)
RoamSwitch
MCPServer
Utilisateur connecté Lire et formater les valeurs de diagnostic, rechercher dans la base de connaissances locale ; les résultats sont renvoyés au client via stdio Modifier les réglages, basculer le verrouillage, isoler un port, éjecter un périphérique. Par défaut, il n'ouvre aucun socket et n'envoie rien sur le réseau (les seules exceptions sont la vérification des signatures npm, désactivée par défaut, et le diagnostic de vulnérabilités visant ce Mac lui-même, 127.0.0.1 ; voir §8). Aucun entitlement d'envoi réseau n'est déclaré, mais l'exécutable n'étant pas sandboxé, cela n'interdit pas en soi les communications

Droits et signature

  • Les trois cibles ont ENABLE_HARDENED_RUNTIME = true.
  • L'application Sandbox est désactivée (com.apple.security.app-sandbox = false). Les droits de l'assistant et de MCPServer sont des dictionnaires vides.
  • Les versions de distribution sont des applications d'ID de développeur signées, notariées par Apple et agrafées (§10).
Compromis de conception

L’App Sandbox n’est pas utilisé. RoamSwitch doit lire l'UUID matériel d'IOKit, utiliser CoreWLAN et DiskArbitration, énumérer les sockets d'écoute des autres processus (lsof), ouvrir un XPC connexion à un LaunchDaemon et générer des binaires système. Rien de tout cela n'est possible à l'intérieur du sandbox, il reste donc désactivé.

Quatre choses compensent cela. Tout d’abord, le runtime renforcé. Deuxièmement, la signature de l'ID de développeur et notarisation. Troisièmement, un seul exécutable s'exécute en tant que root - l'assistant - et ce que cet assistant peut faire est fixe et énuméré (le tableau §3). Quatrièmement, les connexions à l'assistant sont limitées par le code signature (§3).

§3

Comment il est enregistré

L'assistant est enregistré en tant que LaunchDaemon en utilisant SMAppService.daemon(plistName:). Le plist intégré à l'application (Contents/Library/LaunchDaemons/com.tetsuharu.RoamSwitch.Helper.plist) déclare uniquement Label, BundleProgram, une seule entrée MachServices, et AssociatedBundleIdentifiers. En raison du fonctionnement de SMAppService, l'inscription n'est même pas possible à moins que l'application soit en /Applications. Au premier l'enregistrement, l'assistant ne devient actif que lorsque l'utilisateur l'approuve manuellement dans le système Paramètres.

Dont les connexions sont acceptées (ClientValidator)

L'assistant inspecte le processus de connexion dans les NSXPCListener shouldAcceptNewConnection et distribue HelperProtocol uniquement à ceux qui passer. L'inspection utilise le audit_token plutôt que le PID, pour éviter la réutilisation du PID et du TOCTOU.

# Exigences de signature de code imposées par le build Release aux clients qui se connectent
identifier "com.tetsuharu.RoamSwitch"
  and anchor apple generic
  and certificate leaf[subject.OU] = "GV76B6G4YU"

Cette exigence est vérifiée avec SecCodeCopyGuestWithAttributes et SecStaticCodeCheckValidity, et la connexion est interrompue si elle échoue. Seulement Les versions DEBUG suppriment le code PIN d'identification de l'équipe, pour faciliter le développement. Ce qui est réellement expédié est toujours un Version de publication.

Points clés de la frontière

La sécurité de l'assistant repose sur cette seule exigence de signature de code. Tout ce qui le satisfait (un correctement signé RoamSwitch.app) peut appeler toutes les opérations du tableau ci-dessous. Il y a aucun canal pour lui envoyer des commandes arbitraires, mais les opérations de cette table ne sont pas faibles en eux-mêmes. Si RoamSwitch.app lui-même est repris, ces opérations passent au attaquant.

Ce que l'assistant peut faire (la liste complète)

Les opérations privilégiées définies dans Shared/HelperProtocol.swift le sont toutes. Il n’existe aucune API privilégiée qui ne soit répertoriée ici.

HelperProtocol — tout ce qui s'exécute en tant que root
MéthodeCe que ça faitBinaire/API invoquée
setBlockAll(_:) Active ou désactive le pare-feu des applications et le mode furtif /usr/libexec/ApplicationFirewall/socketfilterfw
--setblockall / --setstealthmode
obtenirBlockAllStatus(...) Lit les valeurs actuelles de ce qui précède socketfilterfw --getblockall
setSharingServicesEnabled(_:) déchargement/chargement des démons SSH/SMB/Partage d'écran. A l'arrêt, il enregistre uniquement "ceux qui étaient en cours d'exécution" et ne restitue que ceux /bin/launchctl list / unload -w / load -w
(fixé aux trois : ssh.plist / com.apple.smbd.plist / com.apple.screensharing.plist)
activerNetworkAirGap(...)
disableNetworkAirGap(...)
Applique et lève le bloc complet d'urgence (block drop all). Passe par PFRulesetCoordinator (§4) /sbin/pfctl -f / -e / -sr
setGuardedDevServerPorts(_:) Utilise pf pour bloquer uniquement les connexions external aux ports du serveur de développement donnés (localhost passe par). Passer un tableau vide les soulève tous /sbin/pfctl (également via le coordinateur)
setSecureDNSServers(_:)
restoreOriginalDNSServers(...)
getCurrentDNSServers(...)
Bascule le DNS des services réseau actifs vers un DNS bloquant les logiciels malveillants (Quad9 9.9.9.9 / Cloudflare 1.1.1.2), en sauvegardant les paramètres d'origine et en les restaurant /usr/sbin/networksetup
-listallnetworkservices / -getdnsservers / -setdnsservers
setLinkGuardSinkhole(_:) Lien Garde (§5). Écrit les domaines de phishing/arnaque donnés dans une section gérée délimitée de /etc/hosts sous la forme 0.0.0.0, puis vide le cache DNS. Un tableau vide supprime la section. Les domaines sont normalisés et dédupliqués ; Les adresses IP et les fichiers indésirables sont supprimés ; plafonné à 60 000 ; écrit via un fichier temporaire + remplacement atomique réécrit /etc/hosts (FileManager.replaceItemAt)
/usr/bin/dscacheutil -flushcache
/usr/bin/killall -HUP mDNSResponder
lockGatewayARP(_:)
unlockGatewayARP(...)
getGatewayARPLockStatus(...)
Verrouillage préventif ARP/NDP de la passerelle (§5). Épingle les mappages IP → MAC donnés (la passerelle IPv4, le routeur IPv6 par défaut, les résolveurs DNS en liaison) en tant qu'entrées de cache de voisin permanent. Les formats IP et MAC sont validés ; un modèle de réconciliation (les broches non présentes dans la requête sont supprimées). Le jeu de broches est conservé jusqu'à gateway_arp_lock.json /usr/sbin/arp -s / -d
/usr/sbin/ndp -s / -d
wireGuardImport(_:)
wireGuardForget(...)
Tunnel VPN (§5). L'assistant enregistre/supprime le texte WireGuard .conf à 0600 fichier en écriture seule
wireGuardUp(endpointIPv4:endpointIPv6:port:)
wireGuardDown(...)
wireGuardStatus(...)
Amener le tunnel vers le haut/vers le bas/lire l'état. Le nom d'hôte du point de terminaison est résolu par l'application et l'adresse IP est transmise à l'assistant (le DNS de l'assistant peut être coupé par le kill-switch) Homebrew wg-quick up/down, wg show (wireguard-tools ; désactivé s'il n'est pas installé)
terminerProcess (pid: forceKill :) Suspend (SIGSTOP) ou force la fermeture (SIGKILL) d'un processus. Utilisé pour contenir des processus de type ransomware. pid > 1 seulement appel système kill(2) (pas un sous-processus)
getHelperVersion(...) Renvoie la chaîne de version de l'assistant (utilisée pour les vérifications de compatibilité des applications) —
Compromis de conception

terminateProcess peut envoyer SIGKILL à n'importe quel processus, à condition que pid > 1. setSecureDNSServers accepte n'importe quelle chaîne de serveur DNS. C'est la largeur dont la fonctionnalité a besoin, mais elle n’est pas étroite. Jugez-le en sachant que le le contrôle de signature de code devant lui (ClientValidator) est la seule porte.

État à l'intérieur de l'assistant

  • HelperTool.shared est une instance unique partagée entre les connexions. Auparavant, il s'agissait d'une instance distincte par connexion, donc un confinement d'urgence qui ouvrait une nouvelle connexion pouvait frapper une course qui perdait la trace des « services à ramener ».
  • Les sauvegardes du service de partage et DNS sont uniquement mutées sur une file d'attente série (stateQueue).

Séparation des privilèges dans l'assistant et exception pour la lecture du journal SSH

Juste après son démarrage, l'assistant s'impose à lui-même un plafond appliqué par le noyau (un bac à sable d'exécution, distinct de l'entitlement App Sandbox mentionné plus haut), que les processus enfants qu'il lance héritent. Ce plafond empêche de remplacer des programmes système, d'enregistrer un LaunchDaemon ou un LaunchAgent, de réécrire la configuration de sudoers, PAM ou sshd, d'écrire directement dans le magasin de répertoires local et d'exécuter des programmes situés dans des emplacements où les utilisateurs, les volumes externes ou les dossiers temporaires peuvent écrire. Il n'empêche pas les modifications de comptes faites via Open Directory (dscl) et ne restreint pas le trafic réseau (ce n'est pas son rôle).

Sous ce plafond, /usr/bin/log ne s'exécute pas. Sous macOS 26, log refuse de s'exécuter dans un bac à sable (log: Cannot run while sandboxed) et aussi en tant que nobody (Must be admin). Ni l'app de l'utilisateur connecté ni l'assistant root dans son bac à sable ne peuvent donc lire le journal de sshd (le journal unifié). La détection de la force brute SSH doit lire ce journal ; la séparation des privilèges comporte donc une exception.

L'exception est un petit processus root : avant d'entrer dans son bac à sable, l'assistant lance le même binaire avec --ssh-log-reader. Ce processus n'accepte qu'une commande d'un octet (démarrer ou arrêter) et ne fait rien d'autre que lancer /usr/bin/log avec des arguments fixes (log stream --style ndjson --level info et un filtre fixe qui ne garde que les lignes de sshd). Il ne transmet aucune variable d'environnement, et l'appelant n'a aucun moyen de choisir des arguments ou une commande. Il ne lit ni n'interprète le journal ; la sortie va dans un tube que l'assistant lui a remis.

Les lignes du journal contiennent des chaînes qu'un attaquant peut écrire, comme le nom d'utilisateur ; l'assistant root ne les interprète donc pas. Il confie chaque ligne à un processus d'analyse qui s'exécute en tant que nobody dans un bac à sable interdisant l'accès au réseau, l'écriture de fichiers et le lancement ou la duplication de processus, et l'adresse source est prise à la fin de la ligne (IP port N), que le nom d'utilisateur ne peut pas influencer. Sur un Mac dont la session à distance est désactivée, ce processus ne lance pas log et ne lit rien. Cinq opérations SSH peuvent être appelées par le XPC de l'assistant : appliquer les réglages, récupérer l'état, les enregistrements et les connexions, et lever un blocage. Aucune n'accepte de commande.

§4

Quatre fonctionnalités touchent pf : l'entrefer d'urgence, la protection du port du serveur de développement, le tunnel VPN Le kill-switch WireGuard (§5, 1.7.6+) et son kill-switch Tailscale (§5, 1.8.0+ — uniquement le backend sélectionné). Tous y vont toujours via un point d'entrée unique, PFRulesetCoordinator, et ne jamais exécuter pfctl -f eux-mêmes.

Pourquoi il y a un point d'entrée unique

Auparavant, les deux fonctionnalités chargeaient chacune des règles avec pfctl -f indépendamment, en concurrence sur l'ensemble de règles principal unique de pf. Si la règle étroite block ... port {…} du garde portuaire était chargé après le block drop all de l'entrefer, vous pourriez vous retrouver dans un état où l'écran dit "isolé" mais le Mac était toujours accessible. Ce bug a été trouvé en attaquant réellement le machine d'un autre hôte, et a été corrigé dans la version 1.4.3 (l'histoire est écrite dans docs/marketing/zenn/03_lan_side_attack_test.md).

Comment ça marche maintenant

  • Reconstruit entièrement à chaque fois. L'ensemble des règles requises est reconstruit à partir de l'état actuel et appliqué en une seule fois. Il n’est jamais appliqué comme différence.
  • Une file d'attente série. Chaque modification de pf s'exécute sur le même DispatchQueue, donc qu'elle provienne d'une connexion XPC, du démarrage de l'assistant ou de la minuterie de sécurité, les modifications sont traitées dans l'ordre.
  • L'ordre de priorité est le suivant (les éléments les plus haut priment).
    1. Entrefer de secours → set skip on lo0 et block drop all (rien d'autre n'est pris en compte)
    2. Kill-switch VPN → block drop all plus pass quick pour uniquement : lo, l'interface du tunnel (utunN), la prise de contact UDP avec les IP du point de terminaison épinglé, DHCP et ICMP
    3. Garde du serveur de développement → block drop in quick proto tcp ... port { … }
    4. Aucun → rechargez /etc/pf.conf et remettez pf à son état d'origine
  • Relisez après l'application. pfctl -sr lit les règles pour confirmer que block drop all, ou la règle de chaque port, est réellement chargée. Un cas où pfctl -f a été silencieusement ignoré n'est pas traité comme un succès.
  • Le fichier temporaire est écrit dans un chemin contenant un UUID et supprimé une fois appliqué (la v1.4.5 a abandonné le chemin fixe au profit d'un chemin difficile à deviner). Le répertoire d'état est /Library/Application Support/RoamSwitch.
Comportement de l'API

Les réponses XPC de enableNetworkAirGap et setGuardedDevServerPorts, (Bool, String?), indiquez si l'opération s'est déroulée jusqu'au bout du relecture. L'appelant (tel que ARPSpoofContainmentManager) réessaye en cas d'échec, et si il échoue toujours, il affiche le message directement à l'écran : "Le trafic n'est pas encore arrêté. Éteignez Wi-Fi maintenant."

§5

Les types de blocs sont différents

Types de bloc
GenrePortéeDéclencheurbouclage
Entrefer d'urgence Arrête tout le trafic, entrant et sortant Lorsqu'une activité de chiffrement de type ransomware est détectée. Lors de la détection d'usurpation d'identité ARP, il se déclenche immédiatement uniquement en cas de verrouillage ; sur les réseaux équilibrés/de confiance, il notifie à la place (vous le déclenchez manuellement). Sur un motif de commande connu de type « ClickFix » dans l'historique du shell, comme décoder du base64 directement vers un shell ou AppleScript (1.8.7+, Pro, désactivé par défaut). Non utilisé pour la protection quotidienne hors du domicile Transmis avec set skip on lo0
VPN kill-switch Tout sauf le tunnel, sa poignée de main, DHCP et ICMP Lorsque le tunnel VPN (§5, Pro, désactivé par défaut) est activé et que vous rejoignez un réseau non fiable. Tenu jusqu'à ce que le tunnel soit établi (et pendant qu'il est en panne) Transmis avec set skip on lo0
Garde de port du serveur de développement Seules les connexions TCP external aux ports donnés Une isolation manuelle en un clic ou un blocage automatique lorsqu'un port d'écoute inconnu est détecté (Pro) Depuis localhost, inchangé
Protection quotidienne des réseaux non fiables Pare-feu et furtivité, partage arrêté (§6). pf n'est pas utilisé Lorsque vous vous connectez à un réseau sur lequel vous n'êtes pas enregistré —
Garde de lien Uniquement la résolution de noms de domaines de phishing/arnaque (0.0.0.0 via /etc/hosts). pf n'est pas utilisé Une destination sur le fil des menaces ou un homographe de marque. Activé par défaut (Pro) Non affecté
Verrouillage préventif ARP/NDP Uniquement le MAC de la passerelle, le routeur IPv6 et le DNS en liaison (cache voisin). pf n'est pas utilisé En rejoignant un réseau non fiable (Pro, désactivé par défaut). Ré-épinglé à chaque changement de réseau Non affecté

Comment l'Air-Gap ne s'éternise pas et ne peut pas être attendu

  • La façon dont il se termine dépend de ce qui l'a déclenché (à partir de 1.9.52). Un leurre anti-rançongiciel altéré, une détection XProtect confirmée et un isolement manuel choisi depuis un avertissement ARP sont de « confiance élevée » ; ClickFix, les anomalies de port et similaires sont de « confiance faible ». Un isolement de confiance faible se lève toujours de lui-même au bout de 10 minutes au plus. Un isolement de confiance élevée ne se lève pas à 10 minutes : une levée complète fondée sur le seul temps permettrait à un attaquant de simplement attendre.
  • Un isolement de confiance élevée reste totalement fermé jusqu'à une limite (1 heure par défaut, de 5 minutes à 24 heures dans les réglages) et vérifie alors la cause. Pour un rançongiciel, la cause n'est jugée disparue que si le processus suspect a été identifié et a pris fin, et que les leurres n'ont pas changé depuis la détection. Si cela ne peut être confirmé, il passe en « mode dégradé » : les nouvelles connexions restent bloquées (sauf boucle locale et DHCP) tandis que seule la radio Wi-Fi est rétablie, pour que vous puissiez voir ce qui se passe. Il ne s'ouvre jamais du seul fait du temps écoulé. Il ne s'ouvre que si vous le levez ou si la cause est confirmée disparue.
  • Au début d'un isolement, les connexions déjà ouvertes vers l'extérieur sont aussi coupées. La table d'états de pf est vidée (pfctl -F states), ce qui arrête le trafic antérieur à l'isolement. Il en va de même au passage en mode dégradé.
  • Un isolement causé par une usurpation ARP n'est levé automatiquement que lorsque la disparition de la cause est confirmée sans aucun trafic. La vérification ne fait que lire le cache ARP (elle n'envoie aucun paquet) et juge la cause disparue lorsque la MAC de confiance de la passerelle est vue lors de 3 vérifications consécutives couvrant au moins 60 secondes. Pendant l'isolement, cependant, l'entrée ARP de la passerelle peut disparaître de la table de voisinage (confirmé sur un vrai Mac) ; la cause ne peut alors pas être confirmée. Dans ce cas, l'isolement reste en mode dégradé jusqu'à ce que vous le leviez.
  • Même si l'assistant plante, la vérification de levée ne s'arrête pas. Un LaunchDaemon dédié sans aucun KeepAlive (AirGapFailsafe) démarre seul toutes les 3 minutes et prend la même décision que l'assistant (confiance faible : levée à 10 minutes ; confiance élevée : vérification de la cause à la limite, puis mode dégradé). Si l'assistant plante lui-même et perd son propre minuteur de levée, ce filet de sécurité n'est pas affecté (depuis 1.8.6 ; la décision selon la confiance depuis 1.9.52).
  • Ce démon dédié ne touche jamais à la configuration de démarrage propre de l'assistant interactif. Une tentative antérieure (1.8.5) utilisait le KeepAlive.PathState propre à l'assistant pour cette récupération, mais cela interférait parfois avec une fermeture normale de l'application et provoquait un redémarrage non désiré ; elle a donc été annulée au profit du démon actuel, totalement indépendant.
  • La vérification de l'horodatage utilise une combinaison hybride du temps de fonctionnement monotone (ProcessInfo.systemUptime) et de l'heure murale. Elle privilégie normalement le temps de fonctionnement monotone, de sorte que les corrections NTP et les changements manuels d'horloge n'ont pas d'effet. Elle ne revient à l'heure murale que lorsque le temps de fonctionnement ne peut pas être fiable — juste après un redémarrage, détecté lorsque la valeur actuelle est inférieure à celle stockée.
  • Il est réappliqué après un redémarrage ou la recréation du démon. Au lancement, l'assistant lit l'état sur le disque avec reapplyFromDisk() et restaure lui-même l'Air-Gap, la garde des ports et les réglages système par défaut, dans cet ordre. Il reprend aussi l'état de l'isolement (confiance, complet ou dégradé) et le temps écoulé ; le décompte continue depuis le premier déclenchement (confirmé sur un vrai Mac en redémarrant l'assistant).
  • Un ascenseur échoué est traité comme un échec. Si block drop all n'a pas pu être supprimé, l'horodatage est réécrit afin que le minuteur de sécurité et la nouvelle tentative aient quelque chose sur lequel converger. L'écran n'affiche jamais faussement "levé".
  • Vous pouvez reprendre la main à tout moment : avec « Lever l'isolement Air-Gap » dans la barre des menus, avec le bouton de levée de la fenêtre modale, ou simplement en coupant le Wi-Fi. La levée ne demande pas Pro (elle fonctionne même si Pro expire en plein isolement).

Comment récupérer un faux positif de port-guard

Le blocage automatique des ports inconnus (Pro, activé par défaut une fois Pro activé) peut arrêter un Récepteur LAN — LocalSend, Syncthing, tout a démarré après l'activation de la garde. Quand ça se produit, autorisez-le à partir du bouton "Autoriser" sur la bannière de notification ou de la ligne correspondante dans l'écran "Ports exposés". Un exécutable autorisé une fois est enregistré comme connu et n'est pas bloqué encore une fois (PortAnomalyGuard.allowPort(_:)). Notez que les interpréteurs de script génériques (Python, Node.js, Netcat, etc.) sont strictement limités par path:port plutôt que par binaire seuls pour empêcher les attaques visant à vivre de la terre. Démons du système Apple qui satisfont anchor apple (rapportd, qui soutient Handoff, etc.) ne sont pas regardé en premier lieu.

Appât pour canaris et suppression du confinement par ransomware auto-réparateur

Si les fichiers du leurre Canary sont falsifiés ou renommés et déclenchent l'entrefer d'urgence, libérant le confinement après vérification de la sécurité régénère automatiquement les fichiers Canary manquants ou corrompus revenir à leur hachage de base authentique généré à partir de modèles intégrés, rétablissant immédiatement la surveillance ininterrompue de kqueue (empêchant la contamination de base de l'adversaire). De plus, pendant le confinement actif (lorsque le mode d'urgence est affiché), les alertes de notification redondantes et les événements de redéclenchement issus d'interrogations périodiques d'intégrité en arrière-plan sont automatiquement supprimés pour éviter de distraire l'utilisateur lors de la réponse à un incident.

Détection générique des rançongiciels par entropie — 1.10.0, Pro, activée par défaut

Surveille les dossiers Documents, Bureau, Téléchargements et Photos via FSEvents, et se déclenche lorsque des écritures dont l'entropie de Shannon dépasse le seuil (7,92) arrivent en rafale de 20 fichiers ou plus en 5 secondes. Contrairement à l'approche canari (fichier leurre), elle ne dépend pas de l'altération d'un fichier appât, et détecte donc aussi le chiffrement dans des emplacements sans fichier canari. Ce n'est qu'une fois le seuil atteint qu'elle utilise lsof pour identifier le processus et le comparer à une liste d'autorisation (cp, mv, ditto, tar, ffmpeg, Xcode, Time Machine, principaux navigateurs, etc.). En cas de détection, elle déclenche le même blocage réseau automatique, via RansomwareContainmentManager, que la garde canari.

L'édition Linux peut suivre les rafales par processus, car fanotify associe un PID à chaque événement d'écriture. macOS ne dispose ni de fanotify ni (par choix) d'EndpointSecurity ; cette implémentation ne lit donc que le chemin via FSEvents et compte la rafale globalement, indépendamment du processus. Limite : lorsque plusieurs processus légitimes écrivent simultanément un grand nombre de fichiers à haute entropie (des travaux de compression ou de chiffrement concurrents, par exemple), un faux positif est plus probable ici que sur l'édition Linux.

Instantanés d'avant les dégâts pour la récupération après rançongiciel (Mac)

Un instantané pris au moment de la détection contient déjà les fichiers chiffrés avant celle-ci, il ne permet donc pas de revenir en arrière. RoamSwitch crée donc des instantanés locaux APFS à intervalle régulier, indépendamment de toute détection (toutes les 6 heures par défaut ; désactivé, 1, 3, 6, 12 ou 24 heures). Quand une détection survient, les nouveaux instantanés d'avant les dégâts et le nettoyage sont suspendus jusqu'à 7 jours (mode de conservation), afin que l'état chiffré ne chasse pas la dernière génération d'avant le chiffrement. La récupération est toujours manuelle : seuls les fichiers choisis sont copiés vers ~/RoamSwitch-Recovered/. Les fichiers actuels ne sont jamais écrasés et aucune restauration de tout le volume n'est proposée. La copie exige de monter l'instantané, donc l'assistant privilégié a besoin de l'accès complet au disque (les instantanés sont quand même créés sans lui). macOS peut supprimer seul les instantanés locaux, par exemple quand l'espace libre baisse, et rien ne garantit leur conservation au-delà d'environ 24 heures. La fenêtre de récupération est une fonction Pro (la création des instantanés fonctionne aussi sans Pro).

Bundle de preuves forensiques — 1.10.0

Lorsqu'un Air-Gap d'urgence démarre — déclenché par la garde canari, la garde ransomware par entropie ou une garde similaire — une file d'attente en arrière-plan qui ne bloque pas le confinement lui-même enregistre un instantané de la liste des processus (ps), des connexions réseau (lsof -i), des fichiers modifiés au cours des cinq dernières minutes, et des fichiers ouverts par le processus suspect, dans ~/Library/Application Support/RoamSwitch/incident-evidence/<horodatage>/manifest.json. Le manifeste consigne le SHA-256 de chaque artefact afin qu'une falsification ultérieure puisse être détectée. La liste des bundles est disponible via l'outil MCP get_forensic_evidence_bundles. Limite : conçu sans droit EndpointSecurity, il ne capture ni trace de paquets ni instantané mémoire/descripteurs de fichiers du processus.

Enregistrement des exécutions de processus (Mac, Pro, désactivé par défaut)

L'assistant privilégié lance le /usr/bin/eslogger fourni par macOS (exec, fork, exit) comme processus enfant et enregistre les lancements de processus sur ce seul Mac. Il ne demande pas l'entitlement EndpointSecurity (il n'appelle jamais es_new_client) ; RoamSwitch doit être autorisé dans Accès complet au disque (Réglages Système ; s'il n'apparaît pas, ajoutez /Applications/RoamSwitch.app avec « + »). Tant que l'autorisation manque, il affiche l'état « indisponible », attend et revérifie toutes les 60 secondes (il ne passe pas à un sondage coûteux). Il ne fait que notifier. Il ne bloque ni l'exécution ni la communication et n'isole pas.

Notifie ces dix comportements : un navigateur, une application Office ou de messagerie lançant directement un shell ; l'exécution d'un binaire non signé depuis un dossier temporaire ou un emplacement mis en quarantaine ; une commande sur une ligne qui télécharge puis transmet à un shell (combinée à une URL en IP brute, base64, eval ou une vérification TLS désactivée) ; osascript utilisant base64 ou eval ; l'exécution d'un fichier juste après le retrait de son attribut de quarantaine ; le lancement d'un binaire non signé depuis un enregistrement de démarrage automatique tout juste écrit (LaunchAgent ou LaunchDaemon) ; un parent non signé par Apple lisant un secret du trousseau (comme security find-generic-password -w) ; une commande sur une ligne python3, perl, ruby, php ou pwsh avec -c/-e/-Command, où un décodage base64/eval et un appel à curl, wget, exec ou system apparaissent tous deux sur la même ligne (à partir de 1.10.0 — base64 ou eval seuls ne déclenchent pas cette détection) ; et le lancement d'un processus avec la variable d'environnement DYLD_INSERT_LIBRARIES définie (à partir de 1.10.0 — pour éviter toute fuite, seule la valeur de cette unique variable est lue ; les processus signés par Apple en sont exclus) ; et une ligne de commande qui désigne directement un faux fichier d'identifiants posé par RoamSwitch (comme ~/.aws/credentials ou ~/.env.backup, reconnu par le marqueur qu'il contient), par exemple cat, tar ou curl -F ; les vrais fichiers ne correspondent jamais, et les lectures via une bibliothèque relèvent de la surveillance des honeytokens. Chaque détection porte un identifiant de technique MITRE ATT&CK.

Seules les détections de gravité élevée deviennent des bannières, et celles du même type sont regroupées à une par 10 minutes (chaque événement reste dans l'enregistrement). Les outils de développement comme Claude Code et la CLI GitHub qui lisent leurs propres identifiants ne sont pas notifiés par défaut. Si un autre programme les lit, l'action « Autoriser ce programme » de la notification autorise seulement cet élément (par Team ID s'il est signé, sinon par nom d'exécutable, associé au nom de l'élément).

L'enregistrement est stocké lisible par root seul (dossier 0700, fichiers 0600) et conserve 200 Mo et 14 jours par défaut. Les variables d'environnement ne sont pas stockées, mais les arguments de ligne de commande peuvent contenir des secrets. Chaque segment est scellé dans une chaîne de hachage reliée au hachage du segment précédent, et « Vérifier la chaîne » détecte les modifications, les segments manquants et les scellés supprimés (chacun confirmé sur un vrai Mac). Cela détecte l'altération, sans l'empêcher. La suppression ou le retour en arrière du segment le plus récent ne peut pas être détecté. La lecture passe par le XPC de l'assistant, et MCP passe par l'app (avec une revérification de Pro).

Limites : les exécutions avant le démarrage de l'assistant ou avant l'activation de l'enregistrement ne sont pas enregistrées. eslogger observe après coup et ne peut pas bloquer. Sous une charge telle que des milliers de lancements de processus par seconde, des événements sont perdus (le nombre s'affiche à l'écran et le fonctionnement continue). Cet enregistrement ne remplace pas un EDR (détection et réponse sur les terminaux).

Honeytokens d'identifiants — 1.10.0, Pro, activés par défaut

Place un fichier d'identification leurre, marqué d'une valeur sentinelle, à chacun de ces trois emplacements : ~/.aws/credentials, ~/.ssh/id_rsa_backup et ~/.docker/config.json. Il n'écrase jamais un fichier réel déjà présent à cet emplacement. La détection d'accès combine le même mécanisme kqueue (DispatchSource.makeFileSystemObjectSource) que la garde canari avec un sondage de la date d'accès toutes les 30 secondes, car sans EndpointSecurity un événement open() ne peut pas être capté de manière fiable. La détection ne fait que notifier ; elle ne bloque jamais rien et ne déclenche aucune isolation. Le leurre ssh utilise un nom que ssh ne cherche pas par défaut (et non id_rsa), de sorte qu'une exécution normale de ssh n'est ni perturbée ni signalée à tort. Le leurre AWS utilise un nom de profil distinct au lieu de [default], le leurre Docker pointe vers un registre interne inexistant, et tous sont créés avec le mode 0600 (depuis la 1.10.3). En plus de ces trois emplacements, ~/.pypirc et ~/.env.backup reçoivent aussi un leurre (pour les recherches de jetons PyPI, de fichiers .env et de clés d'API d'IA). ~/.npmrc n'est pas utilisé, car npm le lit à chaque exécution, et les identifiants GCP non plus, car un faux fichier casserait l'authentification réelle. Lorsqu'un leurre est réellement consulté, exactement un leurre associé (~/.aws/config, ~/.ssh/config ou ~/.kube/config) est posé en plus et surveillé lui aussi ; la chaîne s'arrête après une étape, et aucun fichier existant n'est écrasé.

Surveillance de l'accès aux identifiants du navigateur — 1.10.0, opt-in même dans Pro, désactivée par défaut

Surveille, via FSEvents, les écritures dans les bases de données de mots de passe/cookies enregistrés de Chrome, Brave, Edge, Vivaldi, Chromium et Firefox. En cas de rafale, lsof identifie le processus ; tout ce qui n'est pas le navigateur lui-même, un indexeur comme Spotlight, ou un démon de sauvegarde, compte comme une détection. Contrairement aux autres nouvelles gardes, l'activation de Pro ne l'active pas automatiquement — une extension de navigateur ou la fonction d'importation d'un gestionnaire de mots de passe peut la déclencher, si bien que le risque de faux positif est suffisamment inhérent pour qu'elle soit livrée en opt-in.

Communication avec le Sensor (TLS et signatures)

L'appairage, les demandes d'audit et la récupération des résultats avec un RoamSwitch Sensor (un produit distinct) sur le même réseau local utilisent TLS 1.3 avec l'empreinte du certificat (SHA-256) épinglée (Sensor 0.3.9 et ultérieur). L'empreinte n'est pas récupérée par le réseau ; l'utilisateur saisit la valeur affichée sur l'écran du Sensor. L'autorité de certification, le nom d'hôte et l'expiration ne sont pas vérifiés, seule la valeur épinglée est crue. Si TLS échoue, il ne bascule pas en clair (ce repli permettrait à un homme du milieu de rétrograder la connexion). Les signatures des requêtes incluent un horodatage et un nonce (v2) pour empêcher le rejeu. Seul un ancien appairage sans empreinte se connecte encore en clair comme avant, avec un avertissement.

Si le certificat change, la connexion est refusée. La mise à jour de l'épinglage n'est jamais automatique : elle n'est enregistrée que si l'utilisateur saisit et confirme la nouvelle valeur et qu'une sonde signée réussit réellement en TLS. Limite : cette sonde ne vérifie pas par clé que ce Sensor est le même que celui appairé auparavant. Elle suppose que l'utilisateur saisit la valeur qu'il a confirmée sur l'écran du Sensor.

Détection multicouche des téléchargements (signatures statiques + ClamAV) — couche de signatures statiques ajoutée en 1.8.7

Les fichiers nouvellement placés dans les dossiers Téléchargements, Bureau ou Documents passent d'abord par une vérification légère de signatures statiques via StaticSignatureScanner (seulement les 4 premiers Mo, simple correspondance de motifs d'octets AND/OR — aucune expression régulière ni calcul d'entropie). Elle ne nécessite pas l'entitlement EndpointSecurity et continue de fonctionner même là où ClamAV n'est pas installé. La détection est délibérément limitée à ce qui peut être affirmé comme un fait sans corpus de malware réel : la signature de test EICAR, norme du secteur, et cinq reverse shells classiques documentés dans des références publiques d'offensive security (par exemple PayloadsAllTheThings) — /dev/tcp/ de bash/sh, -e de netcat, pty.spawn de Python, Socket de Perl, et fsockopen de PHP. Nous n'écrivons pas de « signatures » au niveau des octets pour des familles de malwares nommées sans disposer d'un échantillon réel dont les extraire — ce ne serait qu'un faux sentiment de sécurité. Les motifs eux-mêmes sont stockés obfusqués par XOR 0x5A, afin que le binaire de RoamSwitch n'intègre jamais littéralement une chaîne EICAR ou un reverse shell que ClamAV pourrait lui-même signaler.

Seuls les fichiers que la couche de signatures statiques juge « propres » passent ensuite à l'analyse ClamAV. Les fichiers nouvellement placés dans les dossiers Téléchargements, Bureau ou Documents sont analysés immédiatement par ClamAV, qu'ils portent ou non une extension .tmp ou un attribut com.apple.quarantine (par exemple une copie faite via le Terminal). EICAR et les autres signatures de test standard du secteur sont traités comme inoffensifs : aucune notification n'est affichée, seule une entrée est ajoutée à l'historique des notifications — et ils ne sont jamais mis en quarantaine ni bloqués (seuls les échantillons de malware réels sont mis en quarantaine). Et si un fichier du même nom avait déjà été mis en quarantaine auparavant, la nouvelle menace est tout de même déplacée vers le dossier de quarantaine sous un nom unique horodaté, afin qu'une menace ne puisse jamais rester à son emplacement d'origine à cause d'une collision de noms.

Détection de web shells dans les dossiers web de développement (Web Rules)

Les fichiers du développement web local (~/Sites, MAMP, Herd, Local, /Library/WebServer/Documents et les dossiers de projet enregistrés pour le rapprochement des CVE de paquets) sont analysés avec les mêmes 18 règles de détection que l'édition Linux : PHP, JavaScript, JSP et ASP. Les règles décrivent uniquement des techniques publiquement documentées et ne contiennent aucune signature tirée d'un échantillon réel. Il y a trois couches : intégrées à l'app, une mise à jour signée en réception seule une fois par jour (incluse dans la récupération unique existante) et vos propres règles dans ~/Library/Application Support/RoamSwitch/rules.d (seuls sont lus les fichiers appartenant à root ou à vous et non modifiables par le groupe ou les autres). Les dossiers déjà surveillés, comme Téléchargements, sont analysés juste après une écriture ; les dossiers web de développement une fois par jour et à la demande (l'analyse quotidienne n'entre pas dans node_modules ni .git ; l'analyse d'un dossier choisi entre dans node_modules). EndpointSecurity n'étant pas utilisé, rien ne peut être arrêté avant son exécution : toute détection est faite après coup.

Par défaut, seul critical est mis en quarantaine automatiquement. Un résultat high laisse le fichier intact et apparaît comme notification et dans une liste des réglages, où vous pouvez choisir « ne plus détecter » (le niveau de quarantaine se règle dans les réglages). Le hook post-détection (~/Library/Application Support/RoamSwitch/hooks/post-detection) ne s'exécute que s'il appartient à root ou à vous et que personne d'autre ne peut l'écrire, et il est arrêté après 30 secondes. La réponse sur les comptes (désactivée par défaut ; l'activer demande le mot de passe administrateur) révoque le shell de connexion du propriétaire d'un fichier mis en quarantaine, mais ne touche ni root, ni les UID inférieurs à 501, ni les utilisateurs connectés (vous compris, qui utilisez ce Mac). Exécuté en root sous le propre bac à sable de l'assistant dans une machine virtuelle macOS 26, le vrai changement de shell par dscl et sa restauration ont été vérifiés (le bac à sable bloque les écritures directes dans dslocal, mais pas les changements faits via dscl). La confirmation par mot de passe administrateur et le chemin d'appel de l'app vers l'assistant n'ont pas encore été vérifiés sur une machine réelle. Il y a eu 0 résultat sur 114 501 fichiers d'un dossier de développement.

Surveillance des nouvelles persistances de démarrage (LaunchAgent/LaunchDaemon) — 1.8.7

Les nouveaux fichiers .plist placés dans ~/Library/LaunchAgents, /Library/LaunchAgents ou /Library/LaunchDaemons sont détectés en temps réel via FSEvents (PersistenceMonitorGuard). Sans ES_EVENT_TYPE_AUTH_CREATE d'EndpointSecurity, l'écriture elle-même ne peut pas être bloquée. Ce qui est possible, c'est de la remarquer en quelques secondes et d'en évaluer le contenu.

L'évaluation porte sur ce qui est directement lancé, et non sur la signature de l'exécutable lui-même. Des techniques réelles observées en 2026 — un LaunchAgent déguisé en Google Update dissimulant un script bash décodé en base64, ou un LaunchDaemon root réexécutant à chaque démarrage une charge utile AppleScript décodée en base64 — cachent toutes deux le code malveillant dans le script lui-même tout en appelant un /bin/bash ou /usr/bin/osascript signé et inchangé. Une vérification ne regardant que la signature de l'exécutable laisserait passer les deux. C'est pourquoi tout LaunchAgent/LaunchDaemon nouvellement enregistré qui invoque directement un interpréteur de script brut (bash, sh, zsh, osascript, python3, perl, ruby, php et similaires) est signalé sans condition, indépendamment de la validité de la signature de cet interpréteur. Les arguments qui lui sont passés sont également soumis à StaticSignatureScanner, et tout motif connu comme malveillant est inclus dans la notification. Les entrées enregistrées via BundleProgram (pointant vers un binaire compilé et signé à l'intérieur d'un bundle d'application — y compris l'assistant propre de RoamSwitch) sont simplement vérifiées de manière habituelle, avec codesign --verify --strict.

Détection de ClickFix et confinement d'urgence — 1.8.7, désactivé par défaut

« ClickFix » est une technique d'ingénierie sociale où un faux CAPTCHA ou écran d'erreur vous demande d'ouvrir le Terminal et d'y coller une commande « pour vérification ». Les détections ont augmenté de plus de 500 % entre 2024 et 2025, et d'ici 2026, cette technique est considérée comme l'un des vecteurs d'attaque macOS les plus répandus. Comme il s'agit de votre propre shell légitime exécutant exactement ce que vous avez saisi, elle échappe entièrement à la vérification de signature de Gatekeeper.

Barre de menu → Protection contre les malwares → Défense ClickFix (Pro, désactivée par défaut). Surveille via FSEvents les nouvelles lignes ajoutées à ~/.zsh_history/~/.bash_history (uniquement les lignes ajoutées après le démarrage de la surveillance — elle ne remonte jamais dans l'historique existant), et ne fait correspondre que les reverse shells classiques qu'elle partage avec StaticSignatureScanner, ainsi que la combinaison spécifique du décodage base64 directement vers un shell ou vers osascript. Un simple curl | bash — extrêmement courant dans les installateurs légitimes — est délibérément exclu, car il est indiscernable des instructions d'installation officielles de Homebrew, rustup, nvm et similaires. Dès qu'une correspondance est trouvée, elle déclenche le même Air-Gap d'urgence décrit plus haut. Comme la commande a déjà été exécutée, il s'agit nécessairement d'une réaction a posteriori, mais elle peut néanmoins interrompre un téléchargement de second stade ou une exfiltration d'identifiants en cours. Comme tout autre déclencheur d'Air-Gap, il se lève de lui-même après 10 minutes au maximum (voir le début de cette section).

Une limite connue. Comme la détection repose sur une écriture dans l'historique du shell, elle ne peut pas détecter la variante sans fichier qui contourne entièrement le Terminal en invoquant directement l'Éditeur de script via le schéma d'URL applescript://, sans rien écrire sur le disque. Apple a mis en place une certaine atténuation pour cela (une boîte de dialogue de confirmation pour les scripts non identifiés), mais des variantes la contournant ont été signalées. Nous avons testé ce chemin sur du matériel réel via le journal unifié de macOS (log show/log stream) et n'avons trouvé aucun signal de journal distinctif sur lequel baser une détection. Nous préférons énoncer clairement cette limite plutôt que revendiquer une couverture que nous n'avons pas.

Détection des risques Docker — 1.8.9, Pro, désactivée par défaut

Barre de menus → « Protection contre les logiciels malveillants » → « Détecter les conteneurs Docker privilégiés et les montages docker.sock » (DockerEventGuard). Comme cette base de code n'a aucun précédent de connexion de streaming persistante telle que docker events, elle utilise la même approche d'interrogation par minuteur que PortAnomalyGuard. Toutes les 20 secondes, elle récupère l'ensemble des identifiants de conteneurs via un léger docker ps -q, puis exécute docker inspect --format uniquement sur la différence (conteneurs nouvellement démarrés) pour une inspection détaillée. La chaîne de format utilisée pour la détection est délibérément identique à celle de l'édition Linux (roamswitch_core::health::LinuxHealthChecker::DOCKER_INSPECT_RISK_FORMAT), de sorte que les deux plateformes signalent exactement les mêmes conditions (démarrage en mode --privileged, ou montage lié /var/run/docker.sock).

Aucune action automatique n'est entreprise lors de la détection. Un conteneur privilégié ou un montage docker.sock est une « configuration » risquée pouvant permettre une évasion de conteneur, mais ce n'est pas une compromission confirmée — il existe aussi des usages légitimes, comme l'exécution délibérée d'un agent de surveillance en mode privilégié. Comme la plupart des utilisateurs n'utilisent pas du tout Docker, cette fonction reste désactivée par défaut même avec une licence Pro. Elle n'utilise pas l'entitlement EndpointSecurity ; la CLI docker est recherchée dans l'emplacement d'installation par défaut de Docker Desktop, puis via Homebrew, puis via which, dans cet ordre. Vérifiée sur une installation réelle de Docker Desktop (29.7.2) selon trois scénarios — un conteneur privilégié, un conteneur avec montage docker.sock et un conteneur normal — sans faux positifs ni détections manquées.

Audit des fuites de secrets et clés API — ajouté en 1.8.4, analyse de dossier ajoutée en 1.8.9

Barre de menus → « Protection contre les logiciels malveillants » → « Audit des fuites de secrets/clés API ». Collez du texte et il est instantanément vérifié à la recherche de clés API et de jetons divulgués, affichant le numéro de ligne, une chaîne masquée et une recommandation par résultat (SecretLeakAuditor, entièrement sur l'appareil). La version 1.8.9 a ajouté une option « Choisir un dossier à analyser », permettant au même moteur de détection d'auditer récursivement un répertoire — comme un dépôt de code source — via auditDirectory(at:). .git, node_modules, target, vendor, dist, build, __pycache__ et venv sont automatiquement exclus, tout comme les fichiers de plus de 2 Mo ou détectés comme binaires. Le traitement s'exécute hors du thread principal pour ne pas bloquer l'interface, et rien n'est jamais envoyé à l'extérieur.

Link Guard (blocage des connexions de phishing) — 1.7.2 et versions ultérieures, renforcé dans la version 1.8.0

Barre de menu → « Protection contre les logiciels malveillants » → « Link Guard » (Pro). Bloque les connexions aux sites de phishing/arnaques sur l'appareil, dans tous les navigateurs et applications. Il existe deux points d'application, utilisés ensemble par ordre de priorité.

  • ① Extension du système de filtrage de contenu (RoamSwitchLinkFilter, 1.8.0+, préférée une fois approuvée). Une extension du système NEFilterDataProvider — pas d'examen Apple (les fournisseurs de filtres de contenu sont en libre-service, pas de file d'attente d'approbation). Il inspecte la résolution réelle du nom du flux TCP sortant après. Le nom de destination provient du nom d'hôte résolu par le système d'exploitation ou, à défaut, du TLS SNI analysé à partir des premiers octets du flux. Ainsi, un navigateur effectuant son propre DoH/DoT et se connectant à une adresse IP nue est toujours bloqué. Il ne réécrit jamais /etc/hosts et attribue chaque flux à un processus. QUIC (UDP/443) n'a pas de SNI lisible, donc en mode block, il est supprimé, forçant le navigateur à revenir à HTTP/2 sur TCP. Nécessite une approbation unique dans les paramètres système.
  • ② Gouffre /etc/hosts (repli, tandis que l'extension n'est pas approuvée/refusée). L'assistant privilégié écrit les domaines cibles dans une section gérée délimitée de /etc/hosts en tant que 0.0.0.0 et vide le cache. Plafonné à 60 000. Une fois l'extension active, cette section est supprimée.
  • Trois modes. "Off" le désactive. "Avertir uniquement" met en pause la connexion correspondante et demande à l'utilisateur (voir Real warn ci-dessous). "Bloquer automatiquement les sites frauduleux évidents (recommandé)" disparaît immédiatement. La valeur par défaut est bloquée à partir de la version 1.7.2.
  • Real warn (1.8.0+, extension uniquement). Sur un warn, appuyez sur l'extension système contient le flux (à la fois le nom du système d'exploitation et le chemin SNI) avec .pause() et l'application génère une notification « Autoriser / Bloquer ». Dès que l'utilisateur atteint allowlist.txt / extra.txt (l'extension interroge les petits fichiers d'état toutes les 1,5 s), les flux suspendus reprennent (autorisent) ou s'arrêtent (bloquent). Aucune réponse dans les 25 s échoue open (un avertissement est consultatif). La décision est mise en cache par hôte (comptez 5 min/bloc 1 h), de sorte que les autres flux de la page et les visites ultérieures sont instantanés et appliqués. La notification block est inchangée : « Bloqué… » avec un bouton « Autoriser une fois (5 min) ».
  • Seuls les cas clairs sont bloqués. Une liste sur le fil de menaces ou un homographe Unicode de marque - tout le reste (TLD à haut risque, usurpation d'identité de sous-domaine, …) est un avertissement. Le moteur de verdict est partagé avec l'édition Linux et n'envoie d'URL nulle part.
  • Récupération d'un mauvais blocage. Autoriser un domaine depuis la notification ou le menu (5 minutes ou permanent). La liste verte est soustraite lorsque la section est régénérée.
  • Pro-gated. L'application (applyMode()) n'a lieu que sur une licence Pro valide. Sans Pro, le mode est stocké mais /etc/hosts n'est jamais touché. L’activation ou l’expiration d’une licence prend effet en milieu de session.
  • Flux et graines groupées. La liste de blocage provient du flux de menaces signé (§7, vérifié avec une clé dédiée au flux). Il fonctionne sur la graine groupée de l'application (~ 60 000 entrées) avant même la première récupération, et la désactivation de la « mise à jour automatique » signifie aucun trafic sortant.

Verrouillage préventif ARP/NDP de la passerelle — 1.7.5 et versions ultérieures

Barre de menu → "Port & Device Monitor" → "Épingler l'ARP/NDP de la passerelle sur des réseaux non fiables (préventif)" (Pro, désactivé par défaut).

  • Comment ça marche. Lors de la connexion à un réseau non fiable, le MAC actuel de la passerelle IPv4, du routeur IPv6 par défaut et des résolveurs DNS en liaison est collecté à partir de route / scutil --dns / arp -n / ndp -an, et l'assistant épingle chacun comme une entrée permanent avec arp -s / ndp -s (confiance à la première utilisation - le premier MAC observé est fiable). Les réponses ARP/NDP usurpées pour ces adresses IP sont ensuite ignorées, de sorte qu'une attaque de l'homme du milieu ne peut pas être mise en place.
  • Scope. Seuls ces trois types d'entrée sont épinglés. Les réseaux de confiance (ouverts) ne sont jamais épinglés. À chaque changement de réseau, il se déverrouille une fois et se réépingle. Les résolveurs publics hors liaison (8.8.8.8, …) n'ont pas d'entrée ARP en liaison et sont automatiquement exclus.
  • arp -s est préventif ; l'entrefer est après coup. Cette broche existe donc une usurpation d'identité ne peut pas réussir ; La détection d'usurpation d'identité ARP (ARPSpoofContainmentManager) et l'entrefer d'urgence existent pour "couper plus vite qu'un humain" une fois que l'on en voit un. Ils fonctionnent de manière indépendante.
  • Persistance. Le jeu de broches est enregistré dans /Library/Application Support/RoamSwitch/gateway_arp_lock.json afin qu'il puisse être déverrouillé lors d'une reconnexion XPC ou d'un redémarrage d'assistance.

Le tunnel VPN (WireGuard / Tailscale) et son kill-switch — 1.7.6 et versions ultérieures, backend sélectionnable en 1.8.0

Barre de menu → "Moniteur de ports et de périphériques" → "Tunnel VPN" (Pro, désactivé par défaut). Il s'agit de la défense anti-MITM primary — elle ne dépend pas de l'intégrité de L2 (ARP/NDP). Le backend est sélectionnable (sous-menu → "Backend") entre "WireGuard (fichier de configuration)" et "Tailscale (Exit Node)". RoamSwitch n'implémente aucune cryptographie elle-même ; seul le backend choisi est armé.

  • (A) Backend WireGuard. Pilote le wireguard-tools de Homebrew (pas de droit à l'extension réseau Apple). Importez votre propre .conf ; l'assistant l'enregistre à 0600. Kill-switch : pf block drop all plus un pass quick uniquement pour lo, l'interface du tunnel, la prise de contact UDP avec les IP du point de terminaison épinglé, DHCP et ICMP. Un AllowedIPs de tunnel non plein déclenche un avertissement de tunnel divisé.
  • (B) Backend Tailscale (1.8.0+). Pour les utilisateurs qui exécutent déjà Tailscale. RoamSwitch not exécute tailscale up / se connecte / installe - il lit tailscale status --json et exécute tailscale set --exit-node=<node>. La version Standalone (téléchargement direct depuis tailscale.com) avec l’intégration CLI activée (Réglages → « CLI integration » → « Install Now », qui ajoute /usr/local/bin/tailscale) est recommandée — la version de l'App Store (GUI) ne peut pas être pilotée depuis l'extérieur de l'application (bac à sable) ; avec lui, vous choisissez le nœud de sortie dans l'application Tailscale et RoamSwitch affiche uniquement l'état. Un nœud de sortie est requis (achemine tout le trafic à travers le tunnel) ; il est automatiquement désarmé s'il est hors ligne ou inaccessible.
  • ⚠️ La version Homebrew (brew install tailscale) n’est pas recommandée. Le tailscaled distribué par Homebrew présente un bug macOS connu qui peut l’empêcher de réécrire correctement la table de routage (tailscale/tailscale#21395, #21216) — un nœud de sortie peut s’afficher comme « connecté » alors que le trafic continue de fuir, ou sa désactivation peut couper entièrement la connexion Internet du Mac. RoamSwitch vérifie la table de routage réelle du système et avertit dans ce cas, mais utiliser la version Standalone ci-dessus évite ce problème dès le départ.
  • Reconfiguration du réseau à la déconnexion. Le macOS autonome tailscaled ne restaure pas proprement le routage + DNS lorsqu'un nœud de sortie est effacé, donc RoamSwitch fait rebondir le(s) service(s) réseau actif (la même chose que basculer le Wi-Fi à la main ; uniquement les services avec une adresse IPv4, sans danger pour les multi-NIC et les IP statiques). Le trafic diminue pendant environ 5 à 10 s et le niveau de protection n'est pas dégradé pendant cette fenêtre.
  • Le kill-switch Tailscale est désactivé par défaut (opt-in). Le nœud de sortie tunnelise déjà tout ; le pf block drop all (autorise uniquement CGNAT/MagicDNS/STUN/DERP/DHCP/ICMP/DNS) est destiné aux utilisateurs avancés et est "résistant aux fuites, pas étanche" (le DNS est autorisé donc tailscaled survit).
  • Automatique / en cas de perte de licence. Apparaît sur les réseaux non fiables, en panne sur les réseaux fiables ; levée à l'expiration de la licence Pro.

§6

Lorsque vous vous connectez à un réseau que vous n'avez pas enregistré, le commutateur « niveau de protection » n'utilise pas pf. Il modifie simplement les paramètres standard du système d'exploitation d'une manière qui peut être inversée ultérieurement.

Opérations par niveau de protection
FonctionnementMise en œuvrePrivilègeComment il est restauré
Pare-feu + mode furtif activé socketfilterfw --setblockall activé / --setstealthmode activé racine (assistant) off lorsque vous revenez à un réseau sécurisé
Arrêtez SSH / SMB / Partage d'écran launchctl décharger -w racine (assistant) Enregistre uniquement ceux qui étaient en cours d'exécution à l'arrêt, et load -w au retour (avec SSH également couplé à /usr/sbin/systemsetup -setremotelogin on pour une restauration macOS moderne garantie)
Désactiver AirDrop les valeurs par défaut écrivent com.apple.sharingd DiscoverableMode Utilisateur (l'application elle-même) Enregistre la valeur précédente et la réécrit au retour

Rien de tout cela n’est un nouveau mécanisme de blocage ajouté par RoamSwitch – il s’agit simplement de basculer les paramètres du système d’exploitation. Si vous supprimez l'application, la seule chose qui s'arrête est la commutation dépendante du réseau ; le dernier système d'exploitation les paramètres appliqués restent tels quels. Rien n'est laissé verrouillé, mais si vous voulez vous tromper Côté sécurité, remettez-le sur « Ouvrir » sur un réseau de confiance avant de le désinstaller.

§7

Ce qui reste sur Mac

Données stockées sur disque
DonnéesEmplacementContenu
Jeton de licencePorte-clés
com.tetsuharu.RoamSwitch.license
Un jeton signé Ed25519. kSecAttrAccessibleAfterFirstUnlock
Paramètres de l'application / garde activée/désactivéeSuite UserDefaults
com.tetsuharu.RoamSwitch
Inscriptions sur des réseaux de confiance, politique de protection, listes d'exclusion, etc.
état pf/Bibliothèque/Support d'application/RoamSwitch/L'horodatage de l'intervalle d'air, le JSON des ports gardés, l'état du kill-switch VPN, le fichier temporaire de l'ensemble de règles appliqué
Flux de menaces Link Guard~/Bibliothèque/Application Support/RoamSwitch/threatfeed/feed.txtLa liste des domaines de phishing/arnaque téléchargés (ou la graine fournie par l'application si elle n'a pas encore été récupérée). La version du flux est dans UserDefaults
Section gérée Link Guard/etc/hôtesUne section délimitée par # BEGIN RoamSwitch link guard… # END, annulant les domaines bloqués à 0.0.0.0. Supprimé lorsque le mode est "Off" (§5)
Goupilles de verrouillage ARP/NDP/Bibliothèque/Support d'application/RoamSwitch/gateway_arp_lock.jsonL'ensemble IP → MAC rendu permanent par le verrouillage préventif (§5). Supprimé une fois déverrouillé
Configuration WireGuard/Bibliothèque/Application Support/RoamSwitch/ (zone d'assistance, 0600)Le .conf importé par l'utilisateur. Le nom d'hôte du point de terminaison est également caché dans UserDefaults (l'application le résout)
UUID de secours de l'appareilParamètres par défaut de l'utilisateurUne valeur aléatoire, générée uniquement lorsque IOKit ne renvoie pas d'UUID (§9)
Journauxos.Logger / NSLogJournalisation unifiée. Rien n'est envoyé à l'extérieur

Trafic qui quitte la machine (la liste complète)

Il n'existe aucun code qui collecte et envoie des résultats de diagnostic, des informations sur les ports, des URL ou des journaux. Aucun SDK d'analyse ni de rapport de crash n'est inclus. La seule bibliothèque externe est Sparkle (mises à jour). Ce qui sort sur le réseau, ce sont les huit flux suivants, auxquels s'ajoutent les fonctions déclenchées par l'utilisateur, les fonctions désactivées par défaut et la protection DNS contre les menaces décrites sous le tableau, et c'est tout (le sixième uniquement si l'utilisateur configure un VPN, le septième uniquement si l'appairage RoamSwitch Sensor est activé).

Connexions sortantes établies par RoamSwitch
ConnexionDestinationQuand ça arriveCe qui est envoyé
Activation/désactivation de licence lafine.net
/api/v1/license/*
Uniquement lorsque l'utilisateur saisit une clé de licence ou désactive Pro Clé de licence, hachage de l'appareil, nom de l'appareil (le « Nom » dans Réglages Système), version de l'application. À la libération, uniquement la clé et le hachage. Les informations personnelles sont traitées par Stripe lors de l'achat ; l'application ne les traite pas
Vérification des mises à jour lafine.net
/updates/appcast.xml
Sparkle vérifie toutes les 24 heures (et aussi au lancement si la dernière vérification date de 24 heures ou plus). Toujours activé par défaut ; aucune information système n'est envoyée Une requête HTTP (un UA et une version standard). L'élément téléchargé est vérifié par la signature EdDSA (§10)
Flux de menaces Link Guard lafine.net
/updates/v1/{manifest, feed/.txt}
Dans la version Pro, lorsque la « Mise à jour automatique » de la protection des liens est activée (par défaut) : toutes les 24 heures (et au lancement). Désactiver la « Mise à jour automatique » supprime ce flux Uniquement des requêtes GET. Aucune chaîne de requête, aucun cookie, rien qui identifie l'appareil (le User-Agent standard contient le nom et la version de l'application ainsi que la version de macOS). Un fichier statique signé en réception seule ; le manifeste et le corps du flux sont vérifiés avec Ed25519. La clé de signature est dédiée au flux et constitue une clé distincte du SUPublicEDKey des mises à jour de l'application (une fuite se limite donc à « une mauvaise liste de blocage »)
Données de mise à jour de la vérification CVE des paquets / de la vérification active des vulnérabilités lafine.net
/updates/v1/manifest
Au lancement de l'application (si le dernier téléchargement date d'au moins 30 jours). La vérification a lieu toutes les 24 heures, et le téléchargement effectif une fois tous les 30 jours. Toujours (les données de mise à jour de la vérification CVE des paquets n'ont pas d'interrupteur, car il s'agit de données purement reçues utilisées uniquement pour une comparaison locale d'inventaire ; cela concerne tous les utilisateurs, y compris la version Free. La carte CVE de la vérification active des vulnérabilités n'est récupérée que si « Vérification active des vulnérabilités (vérification active de l'accessibilité) » est activée) Un simple GET. Aucune chaîne de requête, aucun cookie, rien qui identifie la machine (le User-Agent standard contient le nom et la version de l'application ainsi que la version de macOS). Un fichier statique signé, reçu uniquement ; le manifeste et le contenu du flux sont tous deux vérifiés avec Ed25519. Réutilise la même clé et le même manifeste que le flux de menaces de Link Guard, mais les flux effectivement récupérés (cartes CVE connues pour Homebrew/npm/PyPI/crates.io/RubyGems/Packagist/Go/Maven, la carte CVE de la vérification active des vulnérabilités, et la liste des noms de paquets npm populaires utilisée pour la détection de typosquatting) sont des fichiers distincts
Mise à jour de la définition du virus ClamAV Miroirs officiels ClamAV Lorsque l'utilisateur a installé ClamAV. Dans la version Pro, freshclam est lancé automatiquement au démarrage, toutes les 24 heures et lors des rondes autonomes ; dans la version Free, uniquement lorsque l'on choisit « Mettre à jour les définitions de virus » dans le menu La récupération standard des définitions ClamAV. Elle ne contient aucune information provenant de RoamSwitch (les informations d'identification standard de ClamAV lui-même peuvent être incluses)
Tunnel VPN (§5, WireGuard / Tailscale) Le point de terminaison WireGuard configuré par l'utilisateur, ou le plan de contrôle Tailscale (Tailscale, Inc.) Uniquement lorsque l'utilisateur a configuré le tunnel VPN (Pro, désactivé par défaut) et rejoint un réseau non fiable. Avec WireGuard, le nom d'hôte du point de terminaison est résolu via DNS une fois avant l'ouverture du tunnel. Avec Tailscale, il s'agit du trafic du plan de contrôle géré par le tailscaled déjà installé et connecté par l'utilisateur lui-même — RoamSwitch se contente de lire tailscale status --json et d'exécuter tailscale set --exit-node= Avec WireGuard : la poignée de main WireGuard (UDP) et le trafic à l'intérieur du tunnel. La destination est le propre serveur VPN de l'utilisateur et le contenu est le propre trafic de l'utilisateur. Avec Tailscale : le trafic entre le propre compte Tailscale de l'utilisateur et le plan de contrôle de Tailscale, Inc. (enregistrement de l'appareil, relais DERP, etc., régi par la politique de confidentialité propre de Tailscale). Dans les deux cas, RoamSwitch n'ajoute aucun identifiant ni aucune donnée de diagnostic
Appairage et liaison d'audit RoamSwitch Sensor RoamSwitch Sensor sur le même réseau local (matériel installé par l'utilisateur lui-même, fonctionnant à une IP fixe) Uniquement lorsque « Appairage RoamSwitch Sensor » (Pro, désactivé par défaut) est activé et que vous saisissez l'adresse IP et le code d'appairage du Sensor pour vous appairer (un échange ponctuel). Après l'appairage, il se reconnecte à cette même IP fixe uniquement lorsque vous appuyez sur « Demander un audit au Sensor », et lors de la récupération automatique du résultat (à partir de 5 minutes après la demande, puis toutes les 5 minutes, jusqu'à 5 tentatives) L'appairage envoie le nom d'hôte et la clé publique Ed25519 de ce terminal, et reçoit la clé publique émise par Sensor. Les demandes d'audit et la récupération des résultats sont authentifiées par une signature Ed25519 — seules des demandes signées sont envoyées. Les résultats de diagnostic et les informations de port ne sont reçus que comme réponse du Sensor ; aucun contenu de fichier n'est jamais envoyé. La seule destination est l'adresse IP fixe du Sensor installé par l'utilisateur lui-même — elle n'atteint jamais aucun serveur externe, y compris lafine.net. La confiance mutuelle n'est établie que par l'échange explicite du code d'appairage
Page de paiement Paiement à rayures Uniquement lorsque l'utilisateur appuie sur le bouton d'achat (il s'ouvre dans le navigateur) — (une navigation dans un navigateur)
Portée de « Zero Telemetry »

Ici, « Zero Telemetry » signifie qu'aucune télémétrie ne collecte ni n'envoie de données d'utilisation ou de résultats de diagnostic. Cela ne signifie pas qu'il n'y a aucun trafic réseau. Les huit flux du tableau ci-dessus et les fonctions décrites sous le tableau existent bel et bien. Mais chacun est soit déclenché par l'utilisateur, soit une récupération en réception seule vérifiée par signature, soit une communication avec un service externe choisi par l'utilisateur (les miroirs de distribution de ClamAV, un fournisseur DNS protecteur, l'URL vérifiée, etc.), et les résultats de diagnostic, les ports et le contenu des fichiers du Mac ne sont jamais envoyés au développeur.

Le flux de menaces Link Guard (ligne 3) ajoute « il extrait les mises à jour » en plus de « il n'envoie rien ». défense. Les deux restent séparés ; le livre blanc Linux §1.1 divise également "Zéro Télémétrie" de "recevoir uniquement des mises à jour". Désactivez la « Mise à jour automatique » et Link Guard s'exécute sur le package données (~ 60 000 domaines de phishing/arnaque) plus détection d'homographes hors ligne, et ce chemin ne se produit pas.

Le tunnel VPN (ligne 6) ne se produit que si l'utilisateur configure son propre serveur WireGuard, ou choisit le backend Tailscale et est déjà connecté à tailscaled avec son propre compte Tailscale ; la destination et le contenu sont sous le contrôle de l'utilisateur. RoamSwitch se contente d'ouvrir le tunnel, de maintenir le kill-switch, ou de lire l'état de Tailscale pour changer de nœud de sortie — il n’ajoute aucun identifiant ni aucune donnée d’utilisation. Sans VPN configuré, ce chemin n'existe pas.

L'appairage RoamSwitch Sensor (ligne 7) est une fonctionnalité réservée à Pro, désactivée par défaut ; ce chemin n'existe pas tant que vous ne l'activez pas. L'appairage lui-même n'est établi que par une action explicite — la saisie du code d'appairage émis par l'opérateur du Sensor, avec l'adresse IP fixe du Sensor. Toute communication ultérieure (demandes d'audit, récupération des résultats) est également adressée à cette même IP fixe appartenant à l'utilisateur et authentifiée par une signature Ed25519. Elle n'atteint jamais aucun serveur externe, y compris lafine.net. Les résultats de diagnostic ou le contenu des fichiers ne sont jamais envoyés, si ce n'est en tant que réponse du Sensor lui-même.

Certains flux ne figurent pas dans le tableau. Aucun d'eux n'envoie de résultats de diagnostic ni de contenu de fichiers au développeur. (1) La protection DNS contre les menaces de la version Pro bascule le DNS du système vers Quad9, Cloudflare, AdGuard ou CleanBrowsing (périmètre par défaut : « hors de chez soi / Wi-Fi non fiable »). Tant qu'elle est appliquée, les requêtes de résolution de noms sont envoyées à ce fournisseur. (2) La vérification des signatures npm de la version Pro demande une confirmation à chaque exécution, puis interroge le registre npm. (3) Le diagnostic actif de vulnérabilités (vérification active de l'accessibilité) se limite à ce Mac lui-même (127.0.0.1). (4) La requête HEAD que la fenêtre « Diagnostic de sécurité des liens » envoie directement à l'URL cible est décrite dans le paragraphe suivant.

La feuille de « contrôle de sécurité des liens » intégrée à l'application envoie une requête HEAD à l'URL cible pour voir où atterrit une URL raccourcie (les redirections suivantes vers des adresses privées ou locales sont arrêtées par le v1.4.5 Atténuation SSRF). Le MCP audit_url_safety, en revanche, est une analyse hors ligne qui se termine sur place et n'envoie l'URL nulle part (§8).

Mesuré (2026-08-29) — Verdict : PASS

Ce n'est pas qu'une affirmation. Le 2026-08-29, une installation 1.4.7 en cours d'exécution a été auditée avec tcpdump + attribution par processus (nettop / lsof / capture pktap filtrée) + LuLu, sur une fenêtre d'environ 2 heures, avec le niveau de sécurité fixé sur Verrouillage maximal et la vérification de l'appcast forcée. Résultat (version 1.4.7) : aucun flux sortant attribué à RoamSwitch, RoamSwitchHelper ou RoamSwitchMCPServer autre que la vérification appcast vers lafine.net ; les seuls sockets du serveur MCP étaient vers localhost ; les vidages des entitlements sont vides. Cette mesure ne concerne que la version 1.4.7. Les flux ajoutés depuis sont listés dans le tableau ci-dessus, et une nouvelle mesure de la version actuelle est prévue.

Rédaction complète et script que tout le monde peut exécuter pour le reproduire : audit/RESULTATS-2026-08-29.md

Cette mesure date de la version 1.4.7. Depuis, les flux suivants ont été ajoutés : le flux de menaces de la protection des liens (1.7.2, ligne 3 du tableau), le tunnel VPN (1.7.6, ligne 6), les données de mise à jour de la vérification CVE des paquets (ligne 4) et l'appairage Sensor (ligne 7). En auditant la version actuelle, vous verrez des GET en réception seule vers lafine.net (appcast, flux de menaces, données de mise à jour) ; si un VPN est configuré, vous verrez aussi de l'UDP vers le propre serveur WireGuard de l'utilisateur (destination et contenu sous son contrôle). Le flux de menaces disparaît avec « Mise à jour automatique : désactivée », et le trafic VPN sans VPN configuré (l'appcast et les données de mise à jour de la vérification CVE des paquets n'ont pas d'interrupteur).

§8

RoamSwitchMCPServer est un outil de ligne de commande autonome fourni sur RoamSwitch.app/Contents/MacOS/. Un client MCP tel que Claude Desktop ou Claude Code le lance en tant que sous-processus et lui parle via stdio (JSON-RPC délimité par une nouvelle ligne 2.0). Le SDK officiel ne s'appuierait pas sur le SDK macOS de cette machine, il est donc implémenté à la main sur le JSONSerialization de la Fondation.

Ce que la conception contraint

  • Il est en lecture seule. Il n'existe tout simplement aucune API pour modifier le niveau de sécurité, isoler un port ou éjecter un périphérique. Ce n’est pas quelque chose d’oublié dans la v1 – c’est délibérément laissé de côté. Laisser du code externe (ici, un LLM) réécrire l'état de protection d'un outil de sécurité briserait la confiance de chaque utilisateur.
  • Il n'ouvre aucun socket d'écoute. Il n'enregistre pas de service et n'écoute pas. Il lit une ligne sur stdin, renvoie une ligne sur stdout, puis le client termine le processus.
  • Il n'envoie aucune télémétrie. Seul run_npm_audit_signatures (registre npm), sur option et réservé à Pro sur Mac, communique avec un hôte externe. run_active_vuln_scan (désactivé par défaut) ne sonde que 127.0.0.1, et sur Mac get_exposed_ports se contente d'envoyer un HTTP GET par port exposé vers 127.0.0.1 pour vérifier les en-têtes. Pour retrouver l'adresse MAC de la passerelle par défaut, les outils de diagnostic peuvent envoyer un ping ICMP, normalement un seul (quelques-uns si la MAC reste introuvable), à la passerelle du réseau local.
  • Il lit les paramètres d'un domaine différent. UserDefaults(suiteName: "com.tetsuharu.RoamSwitch") lit explicitement le domaine de l'application (son propre domaine d'ID de bundle est vide). Il ne fait que lire ; ça n'écrit pas.

Les outils qu'il expose

Les principaux outils renvoyés par tools/list et les données qu'ils renvoient (la liste complète se trouve dans Configuration MCP)
OutilCe qu'il retourneTrafic
get_security_reportUn contrôle en 18 points (FileVault / SIP / Gatekeeper / mise à jour automatique / XProtect / pare-feu / chiffrement Wi-Fi / ARP / épinglage ARP de la passerelle / audit de configuration SSH / audit sudo NOPASSWD / ports exposés / configuration des gardes, et plus) avec un score et des conseils de remédiation par point. Chaque point porte aussi les identifiants de contrôle NIST CSF 2.0 et CIS Controls v8 correspondants (à partir de 1.10.0).Local uniquement
verify_security_findingsRéévalue des éléments (checkId) de get_security_report selon l'état actuel et renvoie un verdict par élément, stillPresent (toujours d'actualité), resolved (corrigé, ou sans objet par conception) ou inconclusive (indéterminable), avec un code de motif indépendant de la langue. Il sert à vérifier, après une correction, qu'elle a bien pris effet. Un élément qui n'a pas pu être mesuré, par exemple faute de privilèges, d'assistant privilégié connecté ou d'accès à Docker, n'est jamais déclaré sûr et ressort comme inconclusive. Sans checkIds, tous les éléments sont vérifiés (jusqu'à 50 identifiants sur Mac et 64 sous Linux ; sous Linux, isServer est aussi accepté). Sur Mac, host_firewall, network_stealth_mode, gateway_arp_lock, malware_scanning, dns_threat_guard et usb_zero_trust sont déterminés par les réglages de l'app RoamSwitch. Ils reflètent donc ces réglages et ne constituent pas une nouvelle mesure de l'état réel du système. L'outil est en lecture seule, ne se connecte à aucun hôte externe, ne répare rien et ne modifie aucun réglage. Sous Linux, il n'envoie rien non plus au réseau local et lit seulement la passerelle dans les caches locaux. Sur Mac, pour retrouver l'adresse MAC de la passerelle, il peut envoyer un ping ICMP, normalement un seul (quelques-uns si la MAC reste introuvable), à la passerelle du réseau local.Local uniquement
audit_mcp_configsLit les fichiers de configuration MCP des clients IA (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Cline, Gemini CLI, ainsi que .mcp.json et fichiers similaires dans les dossiers de projet enregistrés) et signale les réglages risqués. Il détecte les outils approuvés automatiquement sans confirmation à chaque appel (auto_approve), les serveurs http:// en clair sur un hôte autre que loopback, que l'adresse soit dans le champ url ou passée comme argument de lancement, par exemple --server http://… (remote_plain_http), les lancements par des commandes shell ou interpréteur d'une ligne (shell_wrapper), les lanceurs de type npx / uvx / docker run qui récupèrent un paquet ou une image non épinglés (unpinned_package_launcher), docker run avec --privileged ou avec le socket Docker ou le système de fichiers racine montés (container_host_access), les variables d'environnement qui font charger du code supplémentaire au démarrage, comme LD_PRELOAD ou --require dans NODE_OPTIONS (noms insensibles à la casse, valeurs jamais renvoyées ; code_loading_env), les entrées qui pointent vers HexStrike AI, un backend d'outils d'attaque sans authentification (hexstrike), les fichiers de configuration modifiables par le groupe ou les autres (world_writable_config), l'option enableAllProjectMcpServers de Claude Code (auto_trust_project_servers) et un fichier de configuration qui contient à la fois un serveur lisant des textes écrits par d'autres (pages web, mails, discussions, issues) et un serveur pouvant exécuter des commandes ou modifier des fichiers, la combinaison dont l'injection de prompt indirecte a besoin (reconnue par des noms de paquets connus, sans voir le contenu des outils des serveurs ; injection_exposure). L'outil n'a aucun argument, est en lecture seule et n'envoie rien au réseau. Il ne renvoie jamais de valeurs d'environnement ou d'en-tête, d'arguments ni de secrets, seulement des noms de serveurs, le nom de base d'une commande, un hôte d'URL et, pour un simple nom de registre, le nom du paquet ou de l'image. Les fichiers impossibles à analyser sont listés dans unreadableFiles et n'ont pas été audités.Aucun
audit_agent_setupLit les fichiers d'instructions et les skills donnés aux agents IA (CLAUDE.md, commands, agents et skills sous ~/.claude, ainsi que ~/.codex, ~/.cursor/rules, ~/.gemini, ~/.agents, ~/.openclaw, et CLAUDE.md, AGENTS.md, .cursorrules et équivalents dans les dossiers de projet enregistrés) et le openclaw.json d'OpenClaw, et signale les contenus et réglages risqués. Dans les fichiers d'instructions et de skills, il signale des phrases cachées dans des caractères de balise Unicode invisibles (hidden_tag_characters), des caractères de largeur nulle et de contrôle de direction (invisible_characters), des formulations qui écrasent des instructions antérieures ou cachent quelque chose à l'utilisateur (override_instruction), le téléchargement-exécution comme curl … | sh (remote_script_exec), le décodage-exécution (encoded_payload_exec), des références à des stockages de clés et d'identifiants (credential_access) à côté d'un envoi ou d'un service de dépôt (credential_exfil_shape, exfil_endpoint), la mise en place de mécanismes persistants (persistence_instruction), du code compilé dans un skill (skill_precompiled_code) et des fichiers modifiables par d'autres (agent_file_writable). Tout ce qui se trouve dans un commentaire HTML est évalué plus haut, car une page rendue le masque. Pour OpenClaw, il signale l'authentification désactivée (openclaw_auth_disabled), une écoute joignable depuis le réseau (openclaw_wide_bind), un jeton de moins de 24 caractères (openclaw_weak_token), une origine générique (openclaw_origin_wildcard), des interrupteurs dangerously… activés (openclaw_dangerous_flag), une liste de proxys qui fait confiance à tout le monde (openclaw_broad_trusted_proxy, openclaw_proxy_trust_loosened), une politique de messages privés ou de groupe ouverte à tous (openclaw_open_dm), mDNS en full (openclaw_mdns_full), des constats d'autodiagnostic masqués (openclaw_audit_suppressed) et des permissions trop larges (openclaw_loose_permissions). Chaque règle est déterministe : une correspondance signifie « une personne devrait lire cette ligne », pas « malveillant ». Il ne prend aucun argument, est en lecture seule et n'envoie rien au réseau. Il ne renvoie jamais le contenu des fichiers ni des secrets (un jeton n'est signalé que par sa longueur), seulement un nom de règle, un numéro de ligne, un hôte d'URL et un chemin de clé de configuration. Les fichiers illisibles figurent dans unreadableFiles et n'ont pas été audités.
get_exposed_portsListe des ports TCP à l'écoute. Pour chaque port exposé au-delà de localhost, l'outil le compare à une base de services dangereux connus et envoie un seul HTTP GET vers 127.0.0.1:port (local, fermé) pour vérifier CORS et les en-têtes.Seule la sonde vers 127.0.0.1
get_guard_statusL'état activé/désactivé des protections de réponse automatique Pro (anomalie de port/ARP/USB/Bluetooth/téléchargement Web+Mail/protection contre les menaces DNS), le niveau de protection actuel et l'état du réseau de confiance.Local uniquement
audit_url_sécuritéUn jugement d'une URL pour le phishing / l'homographe (usurpation d'identité Unicode) / l'usurpation de sous-domaine de marque / TLD à haut risque / HTTP en texte brut. Il est synchrone et entièrement hors ligne (analyzeURL ; il ne suit pas les redirections)Aucun
get_app_helpUne recherche en texte intégral dans la base de connaissances fournie (spécifications des fonctionnalités/paramètres/dépannage/explications des messages de notification)Aucun
audit_secretsDétecte les clés d'API et les clés privées exposées dans un texte, un fichier ou une arborescence (les valeurs trouvées sont masquées en sortie).Aucun
audit_security_logsAgrège les journaux de sécurité récents (Mac : échecs d'authentification sudo, tentatives de force brute SSH, blocages Gatekeeper, détections XProtect, etc. / Linux : échecs d'authentification sudo, force brute SSH, blocages pare-feu, refus AppArmor, détections ClamAV, etc.), masque automatiquement les informations sensibles telles que les clés API et jetons, et renvoie également les résultats de la détection de nouveaux motifs (par gabarits de logs) et des anomalies de fréquence (pics statistiques). Sous Linux, si le journal n'a pas pu être lu (par exemple après un délai dépassé), les zéros du décompte ne signifient pas qu'il ne s'est rien passé ; journalReadComplete passe à false et journalError donne la raison.Local uniquement
run_active_vuln_scanVérification active de vulnérabilités, non destructive et limitée à 127.0.0.1. Aucun autre hôte n'est contacté, mais l'outil envoie des requêtes réseau ; il est donc désactivé par défaut et doit être activé (opt-in) dans les réglages.127.0.0.1 uniquement (désactivé par défaut)
run_package_cve_scanCompare les paquets installés (Mac : Homebrew / Linux : dpkg, dnf, zypper, pacman) à une carte CVE locale. Aucune communication réseau n'a lieu. Sous Linux, si la liste des paquets n'a pas pu être lue ou a expiré, l'outil ne répond pas « aucun résultat » ; complete passe à false et incompleteReason donne la raison (un résultat inachevé n'est pas un résultat sain). Sous Ubuntu (20.04, 22.04, 24.04, 26.04), l'outil utilise les données propres à Ubuntu, compare par nom et version du paquet source, et ne vérifie que les CVE de CVSS 7,0 ou plus pour lesquelles Ubuntu a déjà publié un correctif ; « aucun résultat » ne signifie donc pas qu'il n'y a pas de CVE connue. Le résultat gagne ecosystem et release, chaque détection porte binary (le nom du paquet binaire installé), et les correctifs disponibles seulement via Ubuntu Pro sont marqués proOnly. Les dérivées d'Ubuntu (Linux Mint, Pop!_OS, etc.) et les versions non prises en charge renvoient complete = false avec la raison dans incompleteReason, sans bascule vers les données Debian.Aucun
run_package_cve_scan_languagesCompare les fichiers de verrouillage de dépendances npm, PyPI, crates.io, RubyGems, Packagist, Go et Maven à la même carte CVE locale. Aucune communication réseau n'a lieu.Aucun
get_quarantine_statusRenvoie le contenu du coffre de quarantaine : chemin d'origine, nom de la menace détectée, date de mise en quarantaine et taille.Local uniquement
get_canary_statusRenvoie l'état des fichiers leurres anti-rançongiciel (canary) ainsi que les 50 incidents détectés les plus récents.Local uniquement
get_ransomware_recovery_snapshotsRenvoie les instantanés APFS de récupération (avant les dégâts, à la détection, manuels), celui recommandé pour la restauration, l'état du mode de conservation et l'intervalle des instantanés. Un instantané de détection peut déjà contenir des fichiers chiffrés. Réservé à Pro.Local uniquement
search_exec_eventsRecherche dans l'enregistrement des exécutions de processus (eslogger) par chemin, arguments ou ID de signature, et renvoie l'état de l'enregistreur et si l'enregistrement est lisible. Réservé à Pro.Local uniquement
get_process_treeReconstitue les ancêtres et les descendants d'un processus à partir de l'enregistrement. Réservé à Pro.Local uniquement
get_notification_historyRenvoie l'historique des notifications envoyées par RoamSwitch (anomalies d'audit des journaux de sécurité, détections ClickFix, etc.) des 7 derniers jours, les plus récentes en premier.Local uniquement
get_port_anomaly_incidentsRenvoie l'état de référence de la protection contre les ports anormaux, les ports actuellement bloqués automatiquement et les 50 incidents les plus récents. La réponse précise explicitement que les ports actuellement bloqués sont un instantané de l'état présent, sans horodatage, distinct de l'historique des incidents qui, lui, est horodaté.Local uniquement
get_ssh_guard_incidentsRenvoie l'état et l'historique de la garde contre la force brute SSH : si elle bannit ou ne fait que détecter (par exemple parce qu'un autre outil protège déjà sshd), les sources actuellement bannies avec la durée restante, et jusqu'à 200 enregistrements récents (une source a été bannie, simplement détectée, ou une connexion a réussi après une série d'échecs de la même source). Les noms d'utilisateur essayés ne sont pas conservés sous Linux (seulement leur nombre) et sont renvoyés assainis, jusqu'à 5, sous Mac. Le Mac renvoie aussi les connexions SSH récentes (source, utilisateur, méthode, empreinte de clé). Lecture seule : aucun outil MCP ne lève un bannissement. Sous Linux, la liste des bannissements n'est lisible que par root ; un appel sans root ne renvoie que le nombre. Linux : la Server Edition, et l'édition Client tant que sshd tourne. Mac : Pro uniquement.Local uniquement
get_runtime_threat_statusIndique si ce Mac est isolé (Air-Gap) à la suite d'une détection de logiciel malveillant par XProtect, et l'incident déclencheur. À consulter en premier lors d'un Air-Gap actif.Local uniquement
get_ransomware_entropy_guard_statusRenvoie l'état d'activation de la détection générique des rançongiciels par entropie ainsi que ses incidents récents de rafale d'entropie.Local uniquement
get_honeytoken_statusRenvoie l'état d'activation de la garde honeytoken d'identifiants ainsi que ses événements récents de détection d'accès.Local uniquement
get_browser_credential_watch_statusRenvoie l'état d'activation de la surveillance de l'accès aux identifiants du navigateur ainsi que ses événements récents de détection d'accès.Local uniquement
get_forensic_evidence_bundlesRenvoie la liste des bundles de preuves forensiques (liste des processus, connexions réseau, fichiers modifiés, etc.) enregistrés lors du déclenchement d'un Air-Gap d'urgence.Local uniquement
get_vulnerability_scan_historyRenvoie le dernier résultat enregistré et le nombre de jours écoulés (pass_age) pour chaque sonde enregistrée par run_active_vuln_scan.Local uniquement

Le champ instructions de la réponse initialize indique aussi clairement : « Cannot change security level, isolate ports, or eject devices », communiquant ainsi la limite des capacités au LLM côté client. Les ressources MCP (roamswitch://docs/*) sont également des documents Markdown en lecture seule. 1.10.0 ajoute quatre documents de triage au format agentskills.io (roamswitch://skills/*, en anglais uniquement, par conception).

Une erreur d'attribution découverte — et corrigée — lors d'un exercice avec un LLM local

En septembre 2026, lors d'un exercice (fire drill) où un LLM local (Qwen 3.8 27B) devait enquêter sur un incident via ce serveur MCP, nous avons découvert un cas où le modèle avait mal interprété les données renvoyées par get_port_anomaly_incidents et intégré à tort un ancien blocage de port, sans rapport avec l'incident, dans le récit d'un incident en cours. La cause : un instantané sans horodatage des ports actuellement bloqués était renvoyé dans la même réponse que l'historique réel et horodaté des incidents, sans que rien ne distingue les deux. Le correctif ajoute des champs de note explicites indiquant lequel des deux champs constitue une chronologie et lequel n'est qu'un instantané. En relançant le même scénario avec le même modèle après correction, celui-ci a cité le texte de la note et a explicitement refusé d'attribuer l'ancien port à l'incident en cours. L'efficacité du correctif a été vérifiée à la fois par un test de non-régression automatisé et par une nouvelle exécution avec le véritable LLM local.

Point clé : le code source est public

La source de ce serveur et de la logique de détection qu'il utilise (surveillance ARP, analyse des ports, audit des ports, le bilan de santé en 18 points, analyse de sécurité des URL), est publiée sur github.com/lafine1211/roamswitch-mcp (MIT, un miroir du code livré, étiqueté à chaque publication). Vous pouvez vérifier directement dans le code qu'il est en lecture seule, ce qu'il transmet au LLM, et qu'il n'envoie rien. Il n'inclut pas l'assistant privilégié, le contrôle pf, les gardes qui agissent, ni la licence — ceux-ci restent dans le dépôt de l'application.

Les tests sont également livrés avec lui : tests unitaires en miroir, tests d'entrée contradictoire et fuzzing de mutation, géré par swift test et vérifié dans CI. Le fuzzing a révélé un crash non surveillé (JSONSerialization débordements de pile sur un objet JSON profondément imbriqué) ; c'est réglé avec une vérification de la profondeur d'imbrication avant l'analyseur et enregistrée dans SECURITY_TESTING.md.

§9

Le jeton

  • Il utilise Ed25519 (signatures Curve25519). La clé publique est intégrée dans l'application (LicenseVerifier.embeddedPublicKeyBase64). La clé privée correspondante existe uniquement dans le backend de la licence (une variable d'environnement Firebase Functions) et ne se trouve pas dans le référentiel.
  • Les données signées sont JSON canonique. La signature est créée et vérifiée sur les octets exacts produits par le codage LicensePayload (clé de licence, niveau, hachage de l'appareil, émis à, expiration, nombre de sièges) avec .sortedKeys et .withoutEscapingSlashes de JSONEncoder.
  • Il est conçu pour échouer fermé. Si la clé intégrée est manquante ou mal formée, ou si la signature ou le JSON canonique ne peut pas être produit, le résultat n'est pas "vérifié" - il renvoie invalidSignature.

Liaison de périphérique

device_hash = SHA-256( "RoamSwitch-LifetimeSalt-v1" : lowercase(IOPlatformUUID) )

L'UUID matériel brut n'est pas envoyé au serveur. Dans les rares cas où IOKit ne renvoie pas de UUID, il revient à un UUID aléatoire conservé dans UserDefaults. Au moment de la vérification, si le le device_hash du jeton ne correspond pas au hachage actuel de l'appareil, le résultat est deviceMismatch.

Cela fonctionne hors ligne

Point clé : fonctionne sans serveur

Tout ce que validateSavedLicense() fait au démarrage est de lire le jeton depuis le trousseau et vérifiez-le localement avec la clé publique intégrée. Il ne se connecte pas au réseau. Si le Le serveur de licences est toujours arrêté, les fonctionnalités Pro continuent de fonctionner sur un Mac déjà activé. Le serveur est contacté uniquement pour une nouvelle activation et pour une désactivation explicite. Le l'avis de désactivation au serveur est le meilleur effort - même en cas d'échec, la désactivation locale se termine toujours.

La valeur par défaut est un achat unique (à vie) ; expires_at est coché uniquement lorsque is_lifetime est faux. Le nombre de sièges est exprimé par niveau : 2 pour Personal Pro, 5 pour Équipe.

§10

Signature et légalisation (scripts/release.sh)

  1. Exécutez xcodebuild archive (version, signature manuelle, application d'ID de développeur).
  2. Exportez avec -exportArchive sous method: developer-id.
  3. Après notarytool submit --wait, exécutez stapler staple sur le .app.
  4. Vérifiez avec spctl -a -t exec -vv.
  5. Recompressez après avoir agrafé pour produire l'artefact de mise à jour Sparkle (le ticket de notarisation est donc inclus et il s'exécute hors ligne sans avertissement Gatekeeper).
  6. Construisez le DMG, légalisez et agrafez également le DMG, et vérifiez avec stapler validate.

Mises à jour (Sparkle 2.9.6)

Mettre à jour les paramètres dans Info.plist
CléValeur
SUFeedURLhttps://lafine.net/updates/appcast.xml
Clé SUPublicEDCNxzwijMzMCJzliId76Yl88S/9np6t/xg/zQ9YbYzHs=
SUEnableAutomaticChecksvrai
SUScheduledCheckInterval86400

Avant qu'une mise à jour ne soit appliquée, la signature EdDSA répertoriée dans l'appcast est vérifiée contre le SUPublicEDKey intégré dans l'application. La clé de signature privée existe uniquement dans l’environnement de construction. L'appcast est diffusé via HTTPS. Les mises à jour Delta sont vérifiées par signature de la même manière.

Point clé : l'appcast est conçu pour être public

Le fait que l’URL appcast.xml soit publique n’est pas une faiblesse en soi. Son contenu ne sont que les numéros de version, les notes de version, les URL de téléchargement, la taille des fichiers et la signature EdDSA de chaque construction – rien de secret. L'ancre de confiance n'est pas "l'appcast est-il authentique en transit" mais vérifier la signature de l'artefact avec la clé publique intégrée à l'application. Un attaquant qui peut remplacer complètement l'appcast (MITM, détournement DNS, compromission de l'hébergeur Web) ne peut toujours pas pousser une mise à jour malveillante sans la clé de signature. Gatekeeper (ID de développeur et notarisation) est un deuxième porte.

Deux risques subsistent. La première est que les mises à jour n'arrivent pas, car l'hôte est en panne ou l'appcast est en panne. (aucune mauvaise installation ne se produit – vous n’êtes tout simplement pas mis à jour). L'autre est une attaque par gel qui retient délibérément une mise à jour de sécurité. Sparkle 2.x rejette les rétrogradations et les replays en vérifiant l'ordre des versions, mais une défense complète contre le gel nécessite un serveur de mise à jour dédié avec expiration. C'est sur notre liste de choses à aborder.

§11

Ce que RoamSwitch est censé gérer

  • Sondages et attaques d'un attaquant sur le même réseau local ou d'un appareil IoT compromis. Il répond avec furtivité, audit des ports exposés et isolation de l'extérieur.
  • Exposition sur un réseau auquel vous ne faites pas confiance. Il arrête automatiquement le partage des services et AirDrop.
  • Attaques de l'homme du milieu (usurpation ARP/NDP). Depuis la version 1.7.6, la défense est multicouche : (1) le tunnel VPN + kill-switch (§5, la défense principale, qui ne dépend pas de l'intégrité de la couche L2), (2) le verrouillage préventif de la passerelle par ARP/NDP sur les réseaux non fiables (§5), et (3) la détection d'usurpation avec un air-gap d'urgence (après coup). Le tunnel VPN et le verrouillage de la passerelle sont réservés à Pro et désactivés par défaut. La détection d'usurpation est activée par défaut, et l'air-gap d'urgence l'est dès l'activation de Pro.
  • Rechercher des serveurs de développement et des bases de données (Redis, MongoDB, Elasticsearch, etc.) exposés sur 0.0.0.0 sans authentification et les bloquer de l'extérieur.
  • Détecter rapidement les activités de chiffrement non autorisées de type ransomware et arrêter tout le trafic (cela ne repose pas sur les signatures).
  • BadUSB et protection d'approbation de clavier physique (USBKeyboardGuard, CGEventTap + IOKit) qui intercepte et supprime les frappes des claviers/câbles USB non approuvés (Rubber Ducky, O.MG Cable, etc.) pour empêcher les attaques automatisées par injection de commandes.
  • Une invite d'approbation pour le stockage USB inconnu (un lecteur non reconnu est conservé en lecture seule plutôt que éjecté immédiatement) et une analyse ClamAV automatique sur le stockage connecté (facultatif).
  • Détecte les clés API sensibles (OpenAI, Anthropic, GitHub, AWS, etc.) dans le presse-papiers, entièrement en local, et empêche un collage accidentel dans un formulaire web ou un chat IA avant qu'il ne se produise.
  • Détecte et signale les fichiers de modèles d'IA au format Pickle dangereux (risque d'exécution de code arbitraire) téléchargés depuis Hugging Face ou le web.
  • La signature de test EICAR, norme du secteur, et des reverse shells classiques bien documentés à l'intérieur des fichiers téléchargés (StaticSignatureScanner, à partir de 1.8.7, fonctionne même sans ClamAV installé).
  • Un LaunchAgent/LaunchDaemon nouvellement enregistré qui invoque directement un interpréteur de script brut (PersistenceMonitorGuard, à partir de 1.8.7).
  • Les attaques de type « ClickFix », où un faux écran d'avertissement vous incite à exécuter vous-même une commande dans le Terminal — détectées via des motifs de commandes connus comme malveillants dans l'historique du shell, déclenchant un Air-Gap d'urgence (ClickFixGuard, à partir de 1.8.7, Pro, désactivé par défaut).
  • Détecte et notifie l'instant où un conteneur démarre avec une configuration Docker à risque d'évasion de conteneur, comme le mode --privileged ou un montage lié /var/run/docker.sock (DockerEventGuard, à partir de 1.8.9, Pro, désactivé par défaut, notification uniquement).
  • Audit des fuites de secrets et clés API qui couvre désormais non seulement le texte collé, mais aussi l'analyse récursive de dossiers entiers (SecretLeakAuditor, analyse de dossier ajoutée en 1.8.9, entièrement sur l'appareil).

Ce que nous avons décidé de ne pas faire

  • Il ne remplace pas l'antivirus. ClamAV et XProtect sont utilisés comme auxiliaires ; RoamSwitch à lui seul n'est pas un détecteur de malware à usage général.
  • Nous avons décidé de ne pas demander l'entitlement EndpointSecurity. Les taux d'approbation pour un compte Apple Developer individuel sont faibles, et toute fonctionnalité nécessitant réellement un blocage pré-exécution (par exemple ES_EVENT_TYPE_AUTH_EXEC) est traitée comme un non-objectif fermé tant que cela reste vrai. Plutôt que de bloquer l'exécution elle-même, nous avons mis en œuvre, sans cet entitlement, ce qui est réalisable en pratique en matière de détection a posteriori : détection par signatures statiques, surveillance du démarrage automatique, et défense ClickFix.
  • Ce n'est pas une garantie. Il s'agit d'une couche dans une pile de défense en profondeur, et non de quelque chose qui « empêche complètement les ransomwares ». La copie marketing est également examinée sur cette prémisse.
  • Il ne peut pas protéger une racine ou un noyau déjà compromis. Si un attaquant possède déjà root, il peut également supprimer les règles pf de l'assistant.
  • Il ne restaure pas lui-même l'intégrité L2. La broche préventive ARP/NDP est de confiance lors de la première utilisation : si un attaquant est déjà en place avant votre connexion, il peut épingler un MAC usurpé. Le tunnel VPN (§5) est la réponse lorsque vous ne voulez pas faire cette hypothèse : même avec L2 empoisonné, le contenu est crypté et le kill-switch empêche le texte en clair de fuir. Il ne remplace pas la surveillance DHCP d'entreprise ou l'inspection ARP dynamique.
  • Il ne fournit pas de serveur VPN. La fonctionnalité de tunnel utilise une configuration WireGuard fournie par l'utilisateur ; RoamSwitch ne devient pas un fournisseur VPN.

Surface d'attaque ajoutée par l'installation de RoamSwitch

Surface d'attaque ajoutée et comment elle est contenue
Surface d'attaqueComment il est contenu
Un LaunchDaemon qui s'exécute en tant que root et son service mach (com.tetsuharu.RoamSwitch.Helper) La surface d'opération est fixée à HelperProtocol (le tableau §3). Il n’existe pas de canal de commande arbitraire. Les connexions sont autorisées par une exigence de signature de code, en utilisant audit_token.
Si RoamSwitch.app lui-même est compromis, toutes les opérations de l'assistant sont transférées à l'attaquant Hardened Runtime est activé et l'application ne reçoit aucun privilège inutile. L'envoi de données sur le réseau est limité aux flux du tableau du §7 (et aux fonctions déclenchées par l'utilisateur ou désactivées par défaut décrites sous ce tableau). Aucun entitlement d'envoi réseau n'est déclaré, mais l'application n'étant pas sandboxée, cela n'interdit pas en soi les communications (mesures en fin de §7). Nous prévoyons de faire examiner cela par un tiers.
Les chemins binaires du système générés par l'assistant Les chemins absolus comme /sbin/pfctl sont spécifiés directement, sans dépendance sur PATH. Les arguments sont également codés en dur (à l'exception des numéros de port, des chaînes DNS, des paires ARP IP/MAC et de l'adresse IP du point de terminaison VPN, qui sont tous validés au format).
Réécriture de Link Guard /etc/hosts (setLinkGuardSinkhole) Les écritures sont confinées à une section gérée délimitée ; les lignes à l’extérieur sont conservées textuellement. Les domaines sont normalisés et validés, les adresses IP et les fichiers indésirables sont supprimés, la liste est plafonnée à 60 000 et le fichier est écrit via un fichier temporaire + remplacement atomique. Il ne résout que les cas clairs (liste de menaces ou homographe de marque), décidés par un moteur de verdict uniquement local. La liste de blocage elle-même provient du flux de menaces vérifié par signature (clé dédiée au flux).
Récupération du flux de menaces de Link Guard (un GET quotidien en réception uniquement) Une récupération de fichier statique sans chaîne de requête ni identifiant propre à l'appareil (seulement le User-Agent standard). Le manifeste et le corps du flux sont tous deux vérifiés par Ed25519, et une récupération qui échoue à la vérification est rejetée (pas de repli sur les données non signées). « Mise à jour automatique : désactivée » supprime entièrement le chemin.
Broche ARP/NDP préventive (lockGatewayARP) Seules les entrées du cache voisin pour « la passerelle, le routeur IPv6, le DNS en liaison » sont épinglées. Les paires IP/MAC transmises sont validées au format et réconciliées avec l'ensemble de requêtes (elles n'ajoutent jamais d'entrées seules). Il fonctionne uniquement sur des réseaux non fiables ; Pro, désactivé par défaut. La limite de confiance lors de la première utilisation est indiquée au §11 « Ce que nous avons décidé de ne pas faire ».
Tunnel VPN (wireGuardImport/Up/…, Homebrew wireguard-tools) Le .conf est stocké par l'assistant à 0600. L'adresse IP du point de terminaison est résolue par l'application et transmise à l'assistant (l'assistant ne résout jamais un nom d'hôte arbitraire). La destination et le contenu du tunnel sont sous le contrôle de l'utilisateur. La fonctionnalité est désactivée si wireguard-tools n'est pas installé. Pro, désactivé par défaut. Le kill-switch passe par PFRulesetCoordinator (§4).
Le serveur MCP transmet l'état du système à un LLM (un adjoint confus) Il est en lecture seule, sans API d'écriture implémentée. Les vérifications d'URL sont hors ligne. Le domaine des paramètres est référencé en lecture seule.
Détournement du chemin de mise à jour Vérification de la signature EdDSA (SUPublicEDKey), plus un ticket de notarisation groupé. L'application est diffusée via HTTPS.

Annexe A

Tout ce qui est indiqué dans ce document peut être vérifié par rapport à l'artefact distribué avec les commandes suivantes.

Signature et légalisation

# Signature Developer ID et identifiant d'équipe (Team ID)
codesign -dvvv /Applications/RoamSwitch.app 2>&1 | grep -E 'Authority|TeamIdentifier|flags'

# Si le ticket de notarisation est agrafé (staple)
stapler validate /Applications/RoamSwitch.app
spctl -a -t exec -vvv /Applications/RoamSwitch.app

# Signature du helper embarqué / serveur MCP
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchHelper
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer

Entitlements (vérification qu'aucun entitlement d'envoi réseau n'est déclaré ; l'application n'étant pas sandboxée, cela n'interdit pas en soi les communications)

codesign -d --entitlements :- /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchHelper
codesign -d --entitlements :- /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
# → Un dictionnaire d'entitlements vide. Aucune clé app-sandbox / network client.

Mesurer le trafic

# Exécuter tcpdump en parallèle pour confirmer qu'en usage normal aucun trafic ne sort des flux du tableau
sudo tcpdump -i any -n 'host not 127.0.0.1' and 'not port 53'
# Aucun trafic hormis les flux du tableau (activation de licence, vérifications de mise à jour, données de mise à jour signées, mises à jour ClamAV, etc.)

Pour une vérification plus stricte et automatisée avec attribution par processus, voir rs-zerotel-audit.sh dans roamswitch-support/audit/

Architecture de défense et tests de pénétration (5 limites de défense)

# Vérifie automatiquement en une fois la frontière d'autorisation XPC, la priorité Air-Gap de pf, la détection d'exposition de port, le mode lecture seule MCP et la surveillance ARP
git clone https://github.com/lafine1211/roamswitch-support
cd roamswitch-support/audit
./rs-defense-audit.sh all

Tests d'intrusion en direct et audit de défense multicouche à partir d'une VM macOS : RESULTS-DEFENSE-2026-08-30

Vérification de la protection contre les menaces DNS (logiciels malveillants Quad9 et blocage C2)

# Interroger le domaine de test officiel de Quad9 pour vérifier le blocage des menaces
nslookup test.dns9.quad9.net
# → Doit renvoyer ** server can't find test.dns9.quad9.net: NXDOMAIN, c'est-à-dire que la résolution de noms est bloquée

Vérification Web & Mail Download Guard (détection en temps réel ClamAV)

# Créer la chaîne de test antivirus standard et inoffensive (EICAR) dans Downloads
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > ~/Downloads/eicar_test.com
# → La détection FSEvents déclenche une analyse ClamAV immédiate ; aucune notification n'est affichée, et « signature de test EICAR détectée (inoffensive) »
#   est enregistrée dans l'historique des notifications. EICAR étant un fichier de test standard de l'industrie et non une menace réelle,
#   il n'est ni mis en quarantaine ni bloqué et le fichier reste en place (seul un véritable échantillon de malware est déplacé vers le gestionnaire de quarantaine).

Vérification de la détection par signatures statiques (sans EndpointSecurity)

## Un reverse shell classique tiré de références publiques d'offensive security (inoffensif, jamais exécuté)
echo 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1' > ~/Downloads/rs_test.sh
## → Même sans ClamAV installé, une notification « Fichier téléchargé dangereux mis en quarantaine » apparaît, et le fichier est
##   déplacé immédiatement vers le dossier Quarantine (indépendamment de la base de signatures de ClamAV)

Vérification de la nouvelle surveillance du démarrage automatique (PersistenceMonitorGuard)

# Créer un LaunchAgent inoffensif qui invoque directement un interpréteur de script brut (jamais réellement exécuté)
cat > ~/Library/LaunchAgents/com.example.selfcheck.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.example.selfcheck</string>
  <key>ProgramArguments</key><array><string>/bin/bash</string><string>-c</string><string>echo hi</string></array>
</dict></plist>
EOF
# → En quelques secondes, une notification « Nouvel enregistrement de démarrage automatique détecté » devrait apparaître. Retirez-le ensuite :
rm ~/Library/LaunchAgents/com.example.selfcheck.plist

Link Guard (bloquant les connexions de phishing)

# Si la section gérée est présente (2 lignes BEGIN/END en mode Pro + par défaut)
sudo grep -c 'RoamSwitch link guard' /etc/hosts

# Le contenu de la section et le nombre de sinkholes
sudo sed -n '/BEGIN RoamSwitch link guard/,/END RoamSwitch link guard/p' /etc/hosts | head -4
sudo sed -n '/BEGIN RoamSwitch link guard/,/END RoamSwitch link guard/p' /etc/hosts | grep -c '^0\.0\.0\.0'

# Si le blocage est effectif (vérifié avec une entrée de la section ; inoffensif)
D=$(sudo sed -n '/BEGIN RoamSwitch/,/END RoamSwitch/p' /etc/hosts | awk '/^0\.0\.0\.0/{print $2; exit}')
dscacheutil -q host -a name "$D"   # → ip_address : 0.0.0.0 (résolution de noms bloquée)

# Confirmer également que la section ci-dessus disparaît via Menu → « Protection des liens » → « Désactivé »

Le flux de menaces en réception uniquement (signature vérifiée)

# Flux public et manifeste (n'importe qui peut les récupérer et les vérifier)
curl -s https://lafine.net/updates/v1/manifest        # version/generated/threatfeed{...}
curl -sI https://lafine.net/updates/v1/manifest.sig   # → text/plain
# Seule une requête GET sans chaîne de requête, cookies ni identifiant propre à l'appareil est envoyée (User-Agent standard uniquement). Vérifiable avec tcpdump en parallèle.

Verrouillage préventif ARP/NDP (§5, 1.7.5+)

# Se connecter à un réseau non fiable avec le verrouillage préventif activé → vérifier les entrées permanent
arp -an | grep -i permanent          # La passerelle et les autres IP sont listées comme (permanent)
ndp -an | grep -i 'P '               # Côté IPv6 (P = permanent)
sudo cat "/Library/Application Support/RoamSwitch/gateway_arp_lock.json"  # L'ensemble des correspondances IP→MAC actuellement fixées
# Libérer le verrou via le menu → ce qui précède doit disparaître

Tunnel VPN + kill-switch (§5, 1.7.6+)

# Suppose que wireguard-tools est installé (via Homebrew)
brew list wireguard-tools >/dev/null && echo "wireguard-tools: OK"

# Activer le VPN sur un réseau non fiable → avant l'établissement du tunnel, pf est en état de coupe-circuit
sudo pfctl -sr | grep -E 'block drop all|pass .*(utun|udp)'   # block drop all + un ensemble limité de règles pass quick

# Une fois le tunnel établi (wg-quick up déjà exécuté)
wg show                              # handshake / transfer est actif
route -n get default | grep interface  # → utunN (la route par défaut est le tunnel)
# Désactiver le VPN → le coupe-circuit est levé et pf revient à son état antérieur

L'assistant privilégié lui-même

# Le LaunchDaemon enregistré
sudo launchctl print system/com.tetsuharu.RoamSwitch.Helper

# Les règles pf actuellement chargées (état réel de l'Air-Gap / de la garde de port)
sudo pfctl -sr

# Le répertoire d'état du helper
ls -la "/Library/Application Support/RoamSwitch/"

La réponse du serveur MCP (vérification hors ligne)

BIN=/Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
              '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | "$BIN"
# Renvoie serverInfo et les définitions des outils. Aucune connexion réseau ne se produit.

Les sources et les tests du serveur MCP

git clone https://github.com/lafine1211/roamswitch-mcp
cd roamswitch-mcp
swift build -c release        # La même source que le binaire livré
swift test                    # Tests unitaires, entrées adverses, stdio et fuzzing par mutation
# Voir SECURITY_TESTING.md pour le contenu des tests et les problèmes trouvés

Identifiant de l'appareil

# La valeur brute liée au jeton (seul un SHA-256 salé est jamais envoyé)
ioreg -d2 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}'
Livre blanc Architecture / Sécurité RoamSwitch v1.6 · Champ d'application 1.10.0 (build 116) · Lafine Systems Design · 2026-09-23
Merci de signaler toute erreur ou omission sur lafine.net/contact.html.