Livelli di rete in macOS: guida per sviluppatori
Tour dello stack di rete di macOS pensato per sviluppatori: da socket(2) fino a Network.framework, con note sull'osservabilità a ogni livello.
- Developer tools
- macOS
- Networking
- Deep dive
Stai facendo debug di una chiamata API lenta. I log del server dicono che la risposta è partita in 80 ms. L'utente dice che la pagina ha impiegato quattro secondi. Da qualche parte nello stack tra il filo e l'UI, il tempo è scomparso — e "la rete" è una risposta troppo ampia per essere utile. Per sapere dove guardare, ti serve una mappa mentale dei livelli di rete macOS, dalla chiamata socket BSD in basso al framework che la tua app usa davvero in cima.
Questo è un giro per sviluppatori di come un pacchetto diventa un URLResponse su macOS, dove ogni livello logga e si strumenta, e quale strumento prendere a ciascun livello. È lungo perché lo stack è stratificato per buoni motivi, e vale la pena capirli tutti.
I livelli di rete macOS, dall'alto al basso
Dal basso verso l'alto, lo stack di rete macOS appare grossomodo così:
- Hardware e driver del kernel — famiglie IOKit come
IO80211Familyper Wi-Fi,IOEthernetFamilyper ethernet. - Stack di rete BSD — IP, TCP, UDP, il buffer del socket,
pfper il filtraggio dei pacchetti. - Network Extensions — content filter, packet tunnel provider, DNS proxy, il nuovo macchinario
nw_path. - Syscall
socket(2)— l'API user space più bassa; raramente chiamata direttamente ormai. - CFNetwork / Network.framework — l'API moderna C/Swift per connessioni, path e listener.
- NSURLSession (
URLSession) — il client HTTP/HTTPS di Foundation. Il default per quasi ogni app. - Librerie a livello app — Alamofire, AFNetworking, gRPC-Swift, librerie WebSocket, Apollo, qualsiasi cosa avvolga
URLSession. - Il tuo livello di vista — view SwiftUI, UIKit, AppKit che alla fine renderizzano la risposta.
La maggior parte degli sviluppatori di app passa il tempo al livello 6-8 e tratta tutto sotto come "la rete". Va bene finché qualcosa non si rompe. Quando stai cacciando un bug reale — handshake TLS in timeout, richieste bloccate in proxy auth, traffico sospetto da una dipendenza — devi sapere cosa offre ogni livello di rete macOS e dove logga.
Lo stack basso: BSD, Network Extensions, socket
I tre livelli programmabili più bassi si incastrano strettamente, quindi conviene guardarli come una sola lastra.
Livello 2: lo stack BSD
Quando un pacchetto arriva su en0, il kernel lo fa passare attraverso lo stack di rete BSD. Decide se il pacchetto è per te (incrociando IP e porta di destinazione con un socket), lo passa attraverso pf (il packet filter BSD, il firewall di macOS), e lo accoda sul buffer del socket di ricezione.
Questo livello è dove vivono tcpdump e Wireshark. Si agganciano al device BPF (Berkeley Packet Filter), che vede i pacchetti grezzi prima che il kernel ci abbia fatto molto.
sudo tcpdump -i en0 -n 'tcp port 443'Strumenti utili a livello stack BSD:
netstat -an— socket in ascolto, connessioni stabilite, porte in ascoltonetstat -rn— tabella di routingpfctl -s rules— set di regole pf corrente (spesso vuoto a meno che tu non abbia abilitato il firewall)ifconfig/networksetup -listallnetworkservices— stato dell'interfaccia
Strumenti qui quando sospetti un problema al livello IP/TCP: una tempesta di ritrasmissioni TCP, una route che oscilla, una regola pf che blocca traffico.
Livello 3: Network Extensions
Le Network Extensions sono il modo in cui le app di terze parti e Apple stessa agganciano il data path senza scrivere kext (che sono deprecate). Le grandi categorie:
- Content Filter Provider — vedono i metadati del flusso e decidono allow/deny. Little Snitch e LuLu usano questo.
- Packet Tunnel Provider — tunnellizzazione completa stile VPN. Tailscale, app WireGuard, NordVPN, ecc.
- App Proxy Provider — routing per app attraverso un endpoint remoto.
- DNS Proxy Provider — intercettano le query DNS prima della risoluzione.
Se il tuo traffico va da qualche parte che non ti aspetti, una Network Extension è uno dei colpevoli più probabili. Controlla systemextensionsctl list per le estensioni attive.
Livello 4: socket(2) e amici
L'API POSIX classica: socket(), bind(), connect(), send(), recv(), close(). Quasi mai scrivi questo codice direttamente su macOS più — Apple lo scoraggia per nuove applicazioni perché non si integra con nw_path per i cambi di connettività, non gestisce Happy Eyeballs (dual stack IPv4/IPv6), e non ottiene TLS gratis.
Ma è ancora la fondazione. Ogni API a livello superiore alla fine chiama socket(). Quando leggi l'attribuzione di processo dal kernel — attraverso proc_pidinfo con PROC_PIDFDSOCKETINFO, o attraverso /dev/bpf — stai leggendo lo stato che è stato impostato dalle chiamate socket.
Strumenti qui con:
lsof -i -P -n— ogni socket aperto sul sistema, con PID e nome processo- API
proc_pidinfo— il modo benedetto da Apple per chiedere "quali socket ha aperti questo PID?" dtraceedtruss— tracing a livello di system call (richiede SIP off per script non firmati)- Il template Network di
Instruments— avvolge le sondedtraceper le chiamate socket
Un monitor della larghezza di banda per app come ova legge questo livello. Campiona i contatori byte per PID e per interfaccia del kernel a circa 1 Hz, attribuisce i byte di ogni PID al suo bundle app principale, e memorizza la serie temporale risultante in locale. Non c'è ispezione dei pacchetti — i byte arrivano dagli stessi contatori che il kernel espone a nettop.
Lo stack medio: Network.framework e URLSession
Gran parte del networking a livello app sta in queste due API. Condividono lignaggio ma risolvono problemi diversi.
Livello 5: CFNetwork e Network.framework
CFNetwork è la vecchia API C. Network.framework (introdotto nel 2018, friendly per Swift) è il suo rimpiazzo moderno e quello che Apple raccomanda per qualsiasi nuovo codice di networking di basso livello.
Network.framework ti dà:
NWConnectionper una connessione in uscita (TCP, UDP, QUIC, TLS, protocolli custom)NWListenerper accettare connessioni in entrataNWPathMonitorper osservare la disponibilità del path (Wi-Fi vs cellulare vs ethernet)NWBrowserper la discovery Bonjour- Stack TLS, proxy e protocollo componibili come
NWProtocolFramer
Lo usi direttamente quando scrivi protocolli peer-to-peer, protocolli binari custom, multipath TCP, o implementi software server per macOS. Per tutto il resto a forma HTTP, vai un livello più in alto.
Strumenti a questo livello: NWConnection.stateUpdateHandler, NWPathMonitor per osservare i cambi di path, e il subsystem com.apple.network del log unificato (più sotto sul log unificato).
Livello 6: URLSession
Se la tua app parla HTTP, quasi sicuramente usi URLSession. Gestisce HTTP/1.1, HTTP/2, HTTP/3 (QUIC) dove supportato, TLS, redirect, caching, cookie e challenge di autenticazione. Si integra anche con NSURLProtocol così puoi intercettare il suo traffico per il testing.
Gli hook di strumentazione più utili qui sono:
URLSessionDelegateeURLSessionTaskDelegate— vedi ogni redirect, challenge e completamentoURLSessionTaskMetrics— suddivisione del timing per task: DNS, connect, secure-connect, request, responseURLSessionConfiguration.protocolClasses— registra una sottoclasseNSURLProtocolper loggare ogni richiesta in testos_logcon subsystemcom.apple.CFNetwork— il canale del log unificato a cui CFNetwork emette
URLSessionTaskMetrics è sottoutilizzato. Ti dice esattamente quanto tempo è stato speso in risoluzione DNS vs connect TCP vs handshake TLS vs trasmissione richiesta vs elaborazione server vs trasmissione risposta, per task. Se il tuo problema di chiamata API lenta è in realtà un handshake lento, questi sono i dati che lo dimostrano.
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)
}
}Vedi ova in azione
Un monitor della larghezza di banda nella barra dei menu visibile a colpo d'occhio — locale, firmato, ~3 MB.
La cima dello stack: librerie e il livello vista
I due livelli più alti sono dove gran parte del codice app spende il suo tempo. I bug qui sono facili da scambiare per problemi di rete.
Livello 7: librerie a livello app
Gran parte delle app avvolge URLSession in qualcosa come Alamofire o un modulo di networking custom. Queste librerie di solito espongono un segnale di richiesta-completata che puoi agganciare per il logging. Hanno anche spesso la propria logica di retry, throttling e accodamento che può mascherare problemi a livelli più bassi.
Quando fai debug:
- Controlla se la libreria sta facendo retry automatici — tre retry possono trasformare una richiesta fallita in quattro righe nel log e quattro volte i byte sul filo.
- Controlla la configurazione dei timeout. I default sono spesso sorprendenti (60s per il timeout risorse di
URLSession). - Controlla il parallelismo nascosto.
OperationQueueconmaxConcurrentOperationCount = 1serializzerà le richieste in modi che potresti non aspettarti.
Livello 8: il livello vista
A volte il bug non è proprio nella rete. La risposta è arrivata in 80 ms ma la vista ha impiegato 3,9 secondi a renderizzare perché qualcuno ha messo un decode JSON sincrono sul main thread, o il layout pass è cascato attraverso 800 celle. Il Time Profiler di Instruments è lo strumento giusto per questo — ti mostrerà se il tempo del main thread è stato speso in JSONDecoder.decode o in UIView.layoutSubviews.
La lezione: non incolpare la rete finché non l'hai misurata. URLSessionTaskMetrics più una traccia Time Profiler risolvono gran parte dei dibattiti "pagina lenta" in 10 minuti.
Il log unificato
Attraversando ogni livello c'è il log unificato (os_log / Logger). I subsystem legati alla rete includono:
com.apple.CFNetwork— eventi a livello URLSessioncom.apple.network— eventi Network.frameworkcom.apple.networkd— eventi del daemon di retecom.apple.cfnetwork.NSURLConnection— log legacy di NSURLConnection
Leggere il log di rete live:
log stream --predicate 'subsystem == "com.apple.network"' --level debugLeggere l'ultima ora di eventi CFNetwork:
log show --last 1h --predicate 'subsystem == "com.apple.CFNetwork"'È enormemente utile quando il problema è "la mia richiesta non è mai partita dal dispositivo." Il log ti dirà che il path era unsatisfied, che lo script di autoconfigurazione del proxy ha restituito DIRECT, o che il certificato del server TLS è stato rifiutato con un codice di motivo specifico.
Scegliere lo strumento giusto, e qualche abitudine
Un cheat sheet per "ho un problema a questo livello, quale strumento?":
| Sintomo | Livello probabile | Primo strumento |
|---|---|---|
| L'app non raggiunge nessun host | 2-3 (BSD / NEs) | ping, traceroute, systemextensionsctl list |
| Un host irraggiungibile | 2 (BSD / DNS) | dig, nslookup, dscacheutil -q host |
| Handshake lento | 5-6 (TLS / URLSession) | URLSessionTaskMetrics, log unificato |
| La richiesta non lascia mai il dispositivo | 5-6 (Network.framework) | log unificato com.apple.network |
| Budget traffico per app sconosciuto | 4 (contatori kernel) | nettop, ova |
| Destinazione sospetta | 2-3 (BSD / filtro NE) | lsof -i, content filter |
| Corruzione casuale del body | 6+ (HTTP) | intercettore URLProtocol, cattura pacchetti |
| Render lento dopo risposta veloce | 8 (vista) | Time Profiler di Instruments |
Qualche abitudine che ripaga in tutti questi:
- Logga sempre
taskMetrics. Anche in release. È economico e fa risparmiare ore dopo. - Tagga ogni richiesta in uscita. Imposta un header custom come
X-Request-Ide mettilo nelle righe di log del client e del server. Adesso puoi farne join. - Non fidarti dell'activity indicator. Mostra solo che qualche richiesta è in volo, non quale o da quanto tempo.
- Profila a freddo e a caldo separatamente. La prima richiesta dopo il lancio colpisce DNS, TCP, TLS, e possibilmente la negoziazione HTTP/3. La decima richiesta riusa la connessione ed è 10-50x più veloce. Entrambi i numeri contano.
- Attento al connection coalescing. HTTP/2 e HTTP/3 riuseranno una singola connessione attraverso host che condividono un certificato. Questo rende il ragionamento per host più difficile.
In conclusione
Lo stack dei livelli di rete macOS è denso ma conoscibile. Da socket(2) in basso a URLSession e il tuo livello vista in cima, ogni livello offre una lente specifica — pacchetti, flussi, richieste, task, frame. Scegli la lente giusta e i bug collassano da "la rete è lenta" a "l'handshake TLS verso api.example.com richiede 1,2 secondi perché la catena di certificati viene ri-recuperata ogni volta".
Se vuoi una vista casuale e sempre attiva di quale app sta usando la tua banda adesso — senza nettop che gira in un terminale — installa ova. È un'app nella barra dei menu, intorno a 3 MB, gira su macOS 14 e successivi, campiona a circa 1 Hz, e memorizza tutto in locale. Niente telemetria, niente dashboard remota, pagamento singolo.