Retour au blog
·12 min de lecture·productdevbook

Les couches réseau de macOS : guide du développeur

Tour d'horizon de la pile réseau macOS pour les développeurs : du socket(2) à Network.framework, avec des notes sur l'observabilité à chaque couche.

  • Developer tools
  • macOS
  • Networking
  • Deep dive

Vous déboguez un appel d'API lent. Les logs serveur disent que la réponse est partie en 80 ms. L'utilisateur dit que la page a pris quatre secondes. Quelque part dans la pile entre le câble et l'interface, du temps a disparu — et « le réseau » est une réponse trop large pour être utile. Pour savoir où chercher, il vous faut une carte mentale des couches réseau de macOS, de l'appel BSD socket en bas au framework que votre application utilise réellement en haut.

Voici une visite pour développeur de la façon dont un paquet devient une URLResponse sur macOS, où chaque couche journalise et instrumente, et quel outil attraper à chaque niveau. C'est long parce que la pile est en couches pour de bonnes raisons, et il vaut la peine de toutes les comprendre.

Les couches réseau macOS, de haut en bas

Du bas vers le haut, la pile réseau macOS ressemble grossièrement à cela :

  1. Matériel et pilotes noyau — familles IOKit comme IO80211Family pour le Wi-Fi, IOEthernetFamily pour l'ethernet.
  2. Pile réseau BSD — IP, TCP, UDP, le tampon socket, pf pour le filtrage de paquets.
  3. Network Extensions — filtres de contenu, fournisseurs de tunnel de paquets, proxies DNS, la nouvelle machinerie nw_path.
  4. Syscall socket(2) — l'API userspace la plus basse ; rarement appelée directement aujourd'hui.
  5. CFNetwork / Network.framework — l'API C/Swift moderne pour les connexions, chemins, et listeners.
  6. NSURLSession (URLSession) — le client HTTP/HTTPS de Foundation. Le défaut pour presque chaque application.
  7. Bibliothèques au niveau application — Alamofire, AFNetworking, gRPC-Swift, bibliothèques WebSocket, Apollo, tout ce qui enveloppe URLSession.
  8. Votre couche vue — vues SwiftUI, UIKit, AppKit qui finissent par rendre la réponse.

La plupart des développeurs d'applications passent leur temps aux couches 6-8 et traitent tout en dessous comme « le réseau ». C'est très bien jusqu'à ce que quelque chose casse. Quand vous chassez un vrai bug — poignée de main TLS qui timeoute, requêtes coincées dans l'auth proxy, trafic suspect d'une dépendance — il faut savoir ce que chaque couche réseau de macOS offre et où elle journalise.

La pile basse : BSD, Network Extensions, sockets

Les trois couches programmables du bas s'imbriquent étroitement, donc il vaut la peine de les regarder comme une seule dalle.

Couche 2 : la pile BSD

Quand un paquet arrive sur en0, le noyau le passe à travers la pile réseau BSD. Il décide si le paquet est pour vous (en faisant correspondre l'IP de destination et le port à une socket), le passe à travers pf (le packet filter BSD, le pare-feu macOS), et le met en file dans le tampon de la socket réceptrice.

Cette couche est où vivent tcpdump et Wireshark. Ils tapent dans le périphérique BPF (Berkeley Packet Filter), qui voit les paquets bruts avant que le noyau n'en ait fait grand-chose.

sudo tcpdump -i en0 -n 'tcp port 443'

Outils utiles au niveau de la pile BSD :

  • netstat -an — sockets en écoute, connexions établies, ports en écoute
  • netstat -rn — table de routage
  • pfctl -s rules — ensemble de règles pf actuel (souvent vide sauf si vous avez activé le pare-feu)
  • ifconfig / networksetup -listallnetworkservices — état des interfaces

Vous instrumentez ici quand vous suspectez un problème à la couche IP/TCP : tempête de retransmissions TCP, route qui flappe, règle pf qui bloque le trafic.

Couche 3 : Network Extensions

Les Network Extensions sont la façon dont les applications tierces et Apple lui-même hookent le chemin de données sans écrire de kexts (qui sont obsolètes). Les grandes catégories :

  • Content Filter Providers — voient les métadonnées de flux et décident allow/deny. Little Snitch et LuLu utilisent cela.
  • Packet Tunnel Providers — tunneling complet de style VPN. Tailscale, applications WireGuard, NordVPN, etc.
  • App Proxy Providers — routage par application via un endpoint distant.
  • DNS Proxy Providers — interceptent les requêtes DNS avant la résolution.

Si votre trafic va quelque part où vous ne vous y attendez pas, une Network Extension est l'un des coupables les plus probables. Vérifiez systemextensionsctl list pour les extensions actives.

Couche 4 : socket(2) et amis

L'API POSIX classique : socket(), bind(), connect(), send(), recv(), close(). Vous n'écrivez presque jamais ce code directement sur macOS désormais — Apple décourage cela pour les nouvelles applications parce qu'il ne s'intègre pas avec nw_path pour les changements de connectivité, ne gère pas Happy Eyeballs (dual stack IPv4/IPv6), et n'obtient pas TLS gratuitement.

Mais c'est encore la fondation. Chaque API de plus haut niveau finit par appeler socket(). Quand vous lisez l'attribution de processus depuis le noyau — via proc_pidinfo avec PROC_PIDFDSOCKETINFO, ou via /dev/bpf — vous lisez l'état mis en place par les appels socket.

Vous instrumentez ici avec :

  • lsof -i -P -n — chaque socket ouverte sur le système, avec PID et nom de processus
  • L'API proc_pidinfo — la façon bénie d'Apple de demander « quelles sockets ce PID a-t-il ouvertes ? »
  • dtrace et dtruss — tracing au niveau syscall (exige SIP désactivé pour les scripts non signés)
  • Le template Network d'Instruments — enveloppe les sondes dtrace pour les appels socket

Un moniteur de bande passante par application comme ova lit cette couche. Il échantillonne les compteurs d'octets par PID/socket et par interface du noyau à environ 1 Hz, attribue les octets de chaque PID à son bundle d'application parent, et stocke la série temporelle résultante localement. Pas d'inspection de paquet — les octets viennent des mêmes compteurs que le noyau expose à nettop.

La pile médiane : Network.framework et URLSession

La plupart du réseau au niveau application est dans ces deux API. Elles partagent une lignée mais résolvent des problèmes différents.

Couche 5 : CFNetwork et Network.framework

CFNetwork est l'ancienne API C. Network.framework (introduite en 2018, Swift-friendly) est son remplacement moderne et ce qu'Apple recommande pour tout nouveau code réseau bas niveau.

Network.framework vous donne :

  • NWConnection pour une connexion sortante (TCP, UDP, QUIC, TLS, protocoles personnalisés)
  • NWListener pour accepter les connexions entrantes
  • NWPathMonitor pour observer la disponibilité des chemins (Wi-Fi vs cellulaire vs ethernet)
  • NWBrowser pour la découverte Bonjour
  • TLS, proxy, et piles de protocoles composables comme NWProtocolFramer

Vous l'utilisez directement quand vous écrivez des protocoles peer-to-peer, des protocoles binaires personnalisés, du multipath TCP, ou implémentez des logiciels serveur pour macOS. Pour tout ce qui est en forme HTTP, vous montez d'une couche.

Instrumentation à cette couche : NWConnection.stateUpdateHandler, NWPathMonitor pour observer les changements de chemin, et le sous-système du journal unifié com.apple.network (plus sur le journal unifié ci-dessous).

Couche 6 : URLSession

Si votre application parle HTTP, vous utilisez presque certainement URLSession. Elle gère HTTP/1.1, HTTP/2, HTTP/3 (QUIC) là où c'est supporté, TLS, redirections, cache, cookies, et défis d'authentification. Elle s'intègre aussi avec NSURLProtocol pour que vous puissiez intercepter son trafic pour les tests.

Les hooks d'instrumentation les plus utiles ici sont :

  • URLSessionDelegate et URLSessionTaskDelegate — voir chaque redirection, défi, et complétion
  • URLSessionTaskMetrics — décomposition de timing par tâche : DNS, connect, secure-connect, requête, réponse
  • URLSessionConfiguration.protocolClasses — enregistrer une sous-classe NSURLProtocol pour journaliser chaque requête en test
  • os_log avec sous-système com.apple.CFNetwork — le canal du journal unifié auquel CFNetwork émet

URLSessionTaskMetrics est sous-utilisé. Il vous dit exactement combien de temps a été passé en résolution DNS vs connect TCP vs poignée de main TLS vs transmission de requête vs traitement serveur vs transmission de réponse, par tâche. Si votre problème d'appel d'API lent est en fait une poignée de main lente, c'est la donnée qui le prouve.

func urlSession(_ session: URLSession,
                task: URLSessionTask,
                didFinishCollecting metrics: URLSessionTaskMetrics) {
    for tx in metrics.transactionMetrics {
        print("DNS:", tx.domainLookupEndDate?.timeIntervalSince(tx.domainLookupStartDate ?? .distantPast) ?? 0)
        print("Connect:", tx.connectEndDate?.timeIntervalSince(tx.connectStartDate ?? .distantPast) ?? 0)
        print("TLS:", tx.secureConnectionEndDate?.timeIntervalSince(tx.secureConnectionStartDate ?? .distantPast) ?? 0)
    }
}

Voyez ova en action

Un moniteur de bande passante en barre de menu consultable d'un coup d'œil — local, signé, ~3 Mo.

Télécharger pour macOS

Le sommet de la pile : bibliothèques et couche vue

Les deux couches les plus hautes sont où le code application passe la plupart de son temps. Les bugs ici sont faciles à confondre avec des problèmes réseau.

Couche 7 : bibliothèques au niveau application

La plupart des applications enveloppent URLSession dans quelque chose comme Alamofire ou un module réseau personnalisé. Ces bibliothèques exposent généralement un signal de requête-complétée que vous pouvez hooker pour le logging. Elles ont aussi souvent leur propre logique de retry, de throttling, et de mise en file qui peut masquer les problèmes des couches plus basses.

Lors du débogage :

  • Vérifiez si la bibliothèque fait des retries automatiques — trois retries peuvent transformer une requête échouée en quatre lignes dans le log et quatre fois les octets sur le câble.
  • Vérifiez la configuration de timeout. Les défauts sont souvent surprenants (60 s pour le timeout de ressource URLSession).
  • Vérifiez le parallélisme caché. OperationQueue avec maxConcurrentOperationCount = 1 sérialisera les requêtes de façons que vous pourriez ne pas attendre.

Couche 8 : la couche vue

Parfois le bug n'est pas du tout dans le réseau. La réponse est arrivée en 80 ms mais la vue a pris 3,9 secondes à rendre parce que quelqu'un a collé un décodage JSON synchrone sur le thread principal, ou la passe de layout a cascadé sur 800 cellules. Le Time Profiler d'Instruments est le bon outil pour cela — il vous montrera si le temps du thread principal a été passé dans JSONDecoder.decode ou dans UIView.layoutSubviews.

La leçon : ne blâmez pas le réseau tant que vous ne l'avez pas mesuré. URLSessionTaskMetrics plus une trace Time Profiler règlent la plupart des débats « page lente » en 10 minutes.

Le journal unifié

Coupant à travers chaque couche, il y a le journal unifié (os_log / Logger). Les sous-systèmes liés au réseau incluent :

  • com.apple.CFNetwork — événements au niveau URLSession
  • com.apple.network — événements Network.framework
  • com.apple.networkd — événements du démon réseau
  • com.apple.cfnetwork.NSURLConnection — logs NSURLConnection legacy

Lire le journal réseau en direct :

log stream --predicate 'subsystem == "com.apple.network"' --level debug

Lire la dernière heure d'événements CFNetwork :

log show --last 1h --predicate 'subsystem == "com.apple.CFNetwork"'

C'est énormément utile quand le problème est « ma requête n'a même jamais quitté l'appareil ». Le log vous dira que le chemin était insatisfait, que le script proxy auto-config a renvoyé DIRECT, ou que le certificat serveur TLS a été rejeté avec un code de raison spécifique.

Choisir le bon outil, et quelques habitudes

Une antisèche pour « j'ai un problème à cette couche, quel outil ? » :

SymptômeCouche probablePremier outil
L'application n'atteint aucun hôte2-3 (BSD / NEs)ping, traceroute, systemextensionsctl list
Un hôte injoignable2 (BSD / DNS)dig, nslookup, dscacheutil -q host
Poignée de main lente5-6 (TLS / URLSession)URLSessionTaskMetrics, journal unifié
Requête ne quitte jamais l'appareil5-6 (Network.framework)journal unifié com.apple.network
Budget trafic par application inconnu4 (compteurs noyau)nettop, ova
Destination suspecte2-3 (BSD / filtre NE)lsof -i, filtre de contenu
Corruption aléatoire de corps6+ (HTTP)intercepteur URLProtocol, capture de paquets
Rendu lent après réponse rapide8 (vue)Instruments Time Profiler
Vue de bande passante par application
ova lit les compteurs noyau à la couche socket, regroupe les PIDs auxiliaires sous leur application parente, et vous donne un débit en direct en barre de menu plus un historique parcourable — utile quand « est-ce même du réseau ? » est la première question à laquelle répondre.

Quelques habitudes qui paient à travers tous ces points :

  1. Journalisez toujours taskMetrics. Même en release. C'est peu coûteux et économise des heures plus tard.
  2. Étiquetez chaque requête sortante. Réglez un en-tête personnalisé comme X-Request-Id et mettez-le dans vos lignes de log client et vos lignes de log serveur. Maintenant vous pouvez les joindre.
  3. Ne faites pas confiance à l'indicateur d'activité. Il montre seulement que quelque requête est en vol, pas laquelle ni depuis combien de temps elle est en attente.
  4. Profilez à froid et à chaud séparément. La première requête après le lancement frappe DNS, TCP, TLS, et possiblement la négociation HTTP/3. La dixième requête réutilise la connexion et est 10-50x plus rapide. Les deux nombres comptent.
  5. Faites attention au connection coalescing. HTTP/2 et HTTP/3 réutiliseront une seule connexion à travers les hôtes qui partagent un certificat. Cela rend le raisonnement par hôte plus difficile.

Pour conclure

La pile de couches réseau macOS est dense mais connaissable. De socket(2) en bas à URLSession et votre couche vue en haut, chaque couche offre une lentille spécifique — paquets, flux, requêtes, tâches, frames. Choisissez la bonne lentille et les bugs s'effondrent de « le réseau est lent » à « la poignée de main TLS vers api.example.com prend 1,2 secondes parce que la chaîne de certificats est re-récupérée à chaque fois ».

Si vous voulez une vue décontractée, toujours active, de quelle application utilise votre bande passante maintenant — sans nettop qui tourne dans un terminal — installez ova. C'est une application en barre de menu, environ 3 Mo, tourne sur macOS 14 et ultérieur, échantillonne à environ 1 Hz, et stocke tout localement. Pas de télémétrie, pas de tableau de bord distant, paiement unique.