Torna al blog
·12 min di lettura·productdevbook

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ì:

  1. Hardware e driver del kernel — famiglie IOKit come IO80211Family per Wi-Fi, IOEthernetFamily per ethernet.
  2. Stack di rete BSD — IP, TCP, UDP, il buffer del socket, pf per il filtraggio dei pacchetti.
  3. Network Extensions — content filter, packet tunnel provider, DNS proxy, il nuovo macchinario nw_path.
  4. Syscall socket(2) — l'API user space più bassa; raramente chiamata direttamente ormai.
  5. CFNetwork / Network.framework — l'API moderna C/Swift per connessioni, path e listener.
  6. NSURLSession (URLSession) — il client HTTP/HTTPS di Foundation. Il default per quasi ogni app.
  7. Librerie a livello app — Alamofire, AFNetworking, gRPC-Swift, librerie WebSocket, Apollo, qualsiasi cosa avvolga URLSession.
  8. 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 ascolto
  • netstat -rn — tabella di routing
  • pfctl -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?"
  • dtrace e dtruss — tracing a livello di system call (richiede SIP off per script non firmati)
  • Il template Network di Instruments — avvolge le sonde dtrace per 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à:

  • NWConnection per una connessione in uscita (TCP, UDP, QUIC, TLS, protocolli custom)
  • NWListener per accettare connessioni in entrata
  • NWPathMonitor per osservare la disponibilità del path (Wi-Fi vs cellulare vs ethernet)
  • NWBrowser per 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:

  • URLSessionDelegate e URLSessionTaskDelegate — vedi ogni redirect, challenge e completamento
  • URLSessionTaskMetrics — suddivisione del timing per task: DNS, connect, secure-connect, request, response
  • URLSessionConfiguration.protocolClasses — registra una sottoclasse NSURLProtocol per loggare ogni richiesta in test
  • os_log con subsystem com.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.

Scarica per macOS

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. OperationQueue con maxConcurrentOperationCount = 1 serializzerà 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 URLSession
  • com.apple.network — eventi Network.framework
  • com.apple.networkd — eventi del daemon di rete
  • com.apple.cfnetwork.NSURLConnection — log legacy di NSURLConnection

Leggere il log di rete live:

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

Leggere 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?":

SintomoLivello probabilePrimo strumento
L'app non raggiunge nessun host2-3 (BSD / NEs)ping, traceroute, systemextensionsctl list
Un host irraggiungibile2 (BSD / DNS)dig, nslookup, dscacheutil -q host
Handshake lento5-6 (TLS / URLSession)URLSessionTaskMetrics, log unificato
La richiesta non lascia mai il dispositivo5-6 (Network.framework)log unificato com.apple.network
Budget traffico per app sconosciuto4 (contatori kernel)nettop, ova
Destinazione sospetta2-3 (BSD / filtro NE)lsof -i, content filter
Corruzione casuale del body6+ (HTTP)intercettore URLProtocol, cattura pacchetti
Render lento dopo risposta veloce8 (vista)Time Profiler di Instruments
Vista della banda per app
ova legge i contatori del kernel a livello socket, raggruppa i PID degli ausiliari sotto la loro app principale, e ti dà un rate live nella barra dei menu più una cronologia scorrevole — utile quando "è davvero rete questa?" è la prima domanda a cui rispondere.

Qualche abitudine che ripaga in tutti questi:

  1. Logga sempre taskMetrics. Anche in release. È economico e fa risparmiare ore dopo.
  2. Tagga ogni richiesta in uscita. Imposta un header custom come X-Request-Id e mettilo nelle righe di log del client e del server. Adesso puoi farne join.
  3. Non fidarti dell'activity indicator. Mostra solo che qualche richiesta è in volo, non quale o da quanto tempo.
  4. 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.
  5. 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.