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 :
- Matériel et pilotes noyau — familles IOKit comme
IO80211Familypour le Wi-Fi,IOEthernetFamilypour l'ethernet. - Pile réseau BSD — IP, TCP, UDP, le tampon socket,
pfpour le filtrage de paquets. - Network Extensions — filtres de contenu, fournisseurs de tunnel de paquets, proxies DNS, la nouvelle machinerie
nw_path. - Syscall
socket(2)— l'API userspace la plus basse ; rarement appelée directement aujourd'hui. - CFNetwork / Network.framework — l'API C/Swift moderne pour les connexions, chemins, et listeners.
- NSURLSession (
URLSession) — le client HTTP/HTTPS de Foundation. Le défaut pour presque chaque application. - Bibliothèques au niveau application — Alamofire, AFNetworking, gRPC-Swift, bibliothèques WebSocket, Apollo, tout ce qui enveloppe
URLSession. - 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 écoutenetstat -rn— table de routagepfctl -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 ? » dtraceetdtruss— tracing au niveau syscall (exige SIP désactivé pour les scripts non signés)- Le template Network d'
Instruments— enveloppe les sondesdtracepour 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 :
NWConnectionpour une connexion sortante (TCP, UDP, QUIC, TLS, protocoles personnalisés)NWListenerpour accepter les connexions entrantesNWPathMonitorpour observer la disponibilité des chemins (Wi-Fi vs cellulaire vs ethernet)NWBrowserpour 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 :
URLSessionDelegateetURLSessionTaskDelegate— voir chaque redirection, défi, et complétionURLSessionTaskMetrics— décomposition de timing par tâche : DNS, connect, secure-connect, requête, réponseURLSessionConfiguration.protocolClasses— enregistrer une sous-classeNSURLProtocolpour journaliser chaque requête en testos_logavec sous-systèmecom.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.
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é.
OperationQueueavecmaxConcurrentOperationCount = 1sé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 URLSessioncom.apple.network— événements Network.frameworkcom.apple.networkd— événements du démon réseaucom.apple.cfnetwork.NSURLConnection— logs NSURLConnection legacy
Lire le journal réseau en direct :
log stream --predicate 'subsystem == "com.apple.network"' --level debugLire 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ôme | Couche probable | Premier outil |
|---|---|---|
| L'application n'atteint aucun hôte | 2-3 (BSD / NEs) | ping, traceroute, systemextensionsctl list |
| Un hôte injoignable | 2 (BSD / DNS) | dig, nslookup, dscacheutil -q host |
| Poignée de main lente | 5-6 (TLS / URLSession) | URLSessionTaskMetrics, journal unifié |
| Requête ne quitte jamais l'appareil | 5-6 (Network.framework) | journal unifié com.apple.network |
| Budget trafic par application inconnu | 4 (compteurs noyau) | nettop, ova |
| Destination suspecte | 2-3 (BSD / filtre NE) | lsof -i, filtre de contenu |
| Corruption aléatoire de corps | 6+ (HTTP) | intercepteur URLProtocol, capture de paquets |
| Rendu lent après réponse rapide | 8 (vue) | Instruments Time Profiler |
Quelques habitudes qui paient à travers tous ces points :
- Journalisez toujours
taskMetrics. Même en release. C'est peu coûteux et économise des heures plus tard. - Étiquetez chaque requête sortante. Réglez un en-tête personnalisé comme
X-Request-Idet mettez-le dans vos lignes de log client et vos lignes de log serveur. Maintenant vous pouvez les joindre. - 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.
- 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.
- 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.