Camadas de rede do macOS: guia do desenvolvedor
Um tour focado em desenvolvedores pela pilha de rede do macOS: do socket(2) até o Network.framework, com notas sobre observabilidade em cada camada.
- Developer tools
- macOS
- Networking
- Deep dive
Você está debugando uma chamada de API lenta. Os logs do servidor dizem que a resposta saiu em 80 ms. O usuário diz que a página levou quatro segundos. Em algum lugar na pilha entre a linha e a UI, tempo desapareceu — e "a rede" é uma resposta ampla demais para ser útil. Para saber onde olhar, você precisa de um mapa mental das camadas de rede do macOS, da chamada de socket BSD embaixo até o framework que seu app de fato usa no topo.
Este é um tour de desenvolvedor de como um pacote vira um URLResponse no macOS, onde cada camada loga e instrumenta, e qual ferramenta você pega em cada nível. É longo porque a pilha é em camadas por bons motivos, e vale entender todos.
As camadas de rede do macOS, do topo à base
De baixo para cima, a pilha de rede do macOS se parece com isso:
- Hardware e drivers de kernel — famílias IOKit como
IO80211Familypara Wi-Fi,IOEthernetFamilypara ethernet. - Pilha de rede BSD — IP, TCP, UDP, o buffer do socket,
pfpara filtragem de pacote. - Network Extensions — filtros de conteúdo, packet tunnel providers, DNS proxies, a nova maquinaria
nw_path. - syscall
socket(2)— a API mais baixa do userspace; raramente chamada diretamente hoje. - CFNetwork / Network.framework — a API moderna em C/Swift para conexões, paths e listeners.
- NSURLSession (
URLSession) — o cliente HTTP/HTTPS do Foundation. O padrão para quase todo app. - Bibliotecas de nível de app — Alamofire, AFNetworking, gRPC-Swift, bibliotecas WebSocket, Apollo, qualquer coisa envolvendo
URLSession. - Sua camada de view — views SwiftUI, UIKit, AppKit que finalmente renderizam a resposta.
A maior parte dos devs de app passa o tempo nas camadas 6-8 e trata tudo embaixo como "a rede". Tudo bem até algo quebrar. Quando você está caçando um bug real — handshake TLS dando timeout, requests presos em proxy auth, tráfego suspeito de uma dependência — você precisa saber o que cada uma das camadas de rede do macOS oferece e onde loga.
A pilha mais baixa: BSD, Network Extensions, sockets
As três camadas programáveis mais baixas se encaixam apertado, então vale olhar como uma laje só.
Camada 2: a pilha BSD
Quando um pacote chega em en0, o kernel passa pela pilha de rede BSD. Decide se o pacote é para você (casando IP e porta de destino com um socket), passa pelo pf (o packet filter BSD, o firewall do macOS) e enfileira no buffer do socket receptor.
É nessa camada que tcpdump e Wireshark vivem. Eles se conectam ao dispositivo BPF (Berkeley Packet Filter), que vê pacotes crus antes de o kernel ter feito muito com eles.
sudo tcpdump -i en0 -n 'tcp port 443'Ferramentas úteis no nível da pilha BSD:
netstat -an— sockets em listening, conexões estabelecidas, portas escutandonetstat -rn— tabela de roteamentopfctl -s rules— ruleset atual do pf (frequentemente vazio a menos que você habilite o firewall)ifconfig/networksetup -listallnetworkservices— estado de interfaces
Você instrumenta aqui quando suspeita de um problema na camada IP/TCP: tempestade de retransmissão TCP, rota oscilando, regra pf bloqueando tráfego.
Camada 3: Network Extensions
Network Extensions são como apps de terceiros e a própria Apple gancham o data path sem escrever kexts (que estão deprecadas). As grandes categorias:
- Content Filter Providers — veem metadados de flow e decidem allow/deny. Little Snitch e LuLu usam isso.
- Packet Tunnel Providers — túnel completo estilo VPN. Tailscale, apps WireGuard, NordVPN, etc.
- App Proxy Providers — roteamento por app através de um endpoint remoto.
- DNS Proxy Providers — interceptam queries de DNS antes da resolução.
Se seu tráfego está indo para algum lugar que você não espera, uma Network Extension é uma das culpadas mais prováveis. Cheque systemextensionsctl list para extensões ativas.
Camada 4: socket(2) e amigos
A API POSIX clássica: socket(), bind(), connect(), send(), recv(), close(). Você quase nunca escreve esse código diretamente no macOS hoje — a Apple desencoraja para apps novos porque não integra com nw_path para mudanças de conectividade, não lida com Happy Eyeballs (dual stack IPv4/IPv6) e não pega TLS de graça.
Mas ainda é a fundação. Cada API de mais alto nível eventualmente chama socket(). Quando você lê atribuição de processo do kernel — via proc_pidinfo com PROC_PIDFDSOCKETINFO, ou via /dev/bpf — está lendo estado que foi configurado por chamadas socket.
Você instrumenta aqui com:
lsof -i -P -n— cada socket aberto no sistema, com PID e nome do processo- API
proc_pidinfo— o jeito abençoado da Apple de perguntar "quais sockets esse PID tem abertos?" dtraceedtruss— tracing em nível de syscall (exige SIP off para scripts não assinados)- Template Network do
Instruments— embrulha probes dodtracepara chamadas de socket
Um monitor de banda por app como o ova lê essa camada. Amostra os contadores de bytes por PID e por interface do kernel a aproximadamente 1 Hz, atribui os bytes de cada PID ao bundle do app pai e armazena a série temporal resultante localmente. Não há inspeção de pacote — os bytes vêm dos mesmos contadores que o kernel expõe ao nettop.
A pilha do meio: Network.framework e URLSession
A maior parte da rede em nível de app fica nessas duas APIs. Compartilham linhagem mas resolvem problemas diferentes.
Camada 5: CFNetwork e Network.framework
CFNetwork é a API C antiga. Network.framework (introduzida em 2018, amigável a Swift) é sua substituta moderna e o que a Apple recomenda para qualquer código de rede de baixo nível novo.
Network.framework te dá:
NWConnectionpara uma conexão de saída (TCP, UDP, QUIC, TLS, protocolos custom)NWListenerpara aceitar conexões de entradaNWPathMonitorpara observar disponibilidade de path (Wi-Fi vs celular vs ethernet)NWBrowserpara descoberta Bonjour- TLS, proxy e pilhas de protocolo componíveis como
NWProtocolFramer
Você usa diretamente quando escreve protocolos peer-to-peer, protocolos binários custom, multipath TCP ou implementa software de servidor para macOS. Para tudo formato HTTP, você sobe uma camada.
Instrumentação nessa camada: NWConnection.stateUpdateHandler, NWPathMonitor para observar mudanças de path e o subsystem do unified log com.apple.network (mais sobre o unified log abaixo).
Camada 6: URLSession
Se seu app fala HTTP, quase certamente usa URLSession. Ele lida com HTTP/1.1, HTTP/2, HTTP/3 (QUIC) onde suportado, TLS, redirects, cache, cookies e desafios de autenticação. Também integra com NSURLProtocol para você poder interceptar tráfego para teste.
Os hooks de instrumentação mais úteis aqui são:
URLSessionDelegateeURLSessionTaskDelegate— veem cada redirect, desafio e completionURLSessionTaskMetrics— detalhamento de timing por task: DNS, connect, secure-connect, request, responseURLSessionConfiguration.protocolClasses— registre uma subclasse deNSURLProtocolpara logar cada request em testeos_logcom subsystemcom.apple.CFNetwork— o canal do unified log para o qual o CFNetwork emite
URLSessionTaskMetrics é subutilizado. Te diz exatamente quanto tempo foi gasto em resolução DNS vs connect TCP vs handshake TLS vs transmissão de request vs processamento do servidor vs transmissão de resposta, por task. Se seu problema de chamada de API lenta é na verdade um handshake lento, esses são os dados que provam.
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)
}
}Veja o ova em ação
Um monitor de banda na barra de menu para olhar de relance — local, assinado, ~3 MB.
O topo da pilha: bibliotecas e a camada de view
As duas camadas mais altas são onde a maior parte do código de app passa o tempo. Bugs aqui são fáceis de confundir com problemas de rede.
Camada 7: bibliotecas de nível de app
A maior parte dos apps embrulha URLSession em algo como Alamofire ou um módulo de rede custom. Essas bibliotecas costumam expor um sinal de request-completed que você pode ganchar para logging. Também muitas vezes têm a própria lógica de retry, throttling e queuing que pode mascarar problemas em camadas mais baixas.
Ao debugar:
- Cheque se a biblioteca está fazendo retries automáticos — três retries podem virar um request falhado em quatro linhas no log e quatro vezes os bytes na linha.
- Cheque a configuração de timeout. Padrões costumam surpreender (60s para resource timeout do
URLSession). - Cheque paralelismo escondido.
OperationQueuecommaxConcurrentOperationCount = 1vai serializar requests de formas que você pode não esperar.
Camada 8: a camada de view
Às vezes o bug nem está na rede. A resposta chegou em 80 ms mas a view levou 3,9 segundos para renderizar porque alguém colocou um decode JSON síncrono na main thread, ou o layout pass cascateou por 800 cells. O Time Profiler do Instruments é a ferramenta certa para isso — vai te mostrar se o tempo de main thread foi gasto em JSONDecoder.decode ou em UIView.layoutSubviews.
A lição: não culpe a rede até ter medido. URLSessionTaskMetrics mais um trace do Time Profiler resolvem a maior parte dos debates de "página lenta" em 10 minutos.
O unified log
Atravessando cada camada está o unified log (os_log / Logger). Subsystems relacionados a rede incluem:
com.apple.CFNetwork— eventos no nível URLSessioncom.apple.network— eventos do Network.frameworkcom.apple.networkd— eventos do daemon de redecom.apple.cfnetwork.NSURLConnection— logs legacy do NSURLConnection
Lendo o log de rede ao vivo:
log stream --predicate 'subsystem == "com.apple.network"' --level debugLendo a última hora de eventos CFNetwork:
log show --last 1h --predicate 'subsystem == "com.apple.CFNetwork"'Isso é enormemente útil quando o problema é "meu request nem saiu do dispositivo". O log vai te dizer que o path estava unsatisfied, que o script de auto-config do proxy retornou DIRECT ou que o certificado de servidor TLS foi rejeitado com um código de motivo específico.
Escolhendo a ferramenta certa, e alguns hábitos
Uma cola para "tenho um problema nessa camada, qual ferramenta?":
| Sintoma | Camada provável | Primeira ferramenta |
|---|---|---|
| App não alcança nenhum host | 2-3 (BSD / NEs) | ping, traceroute, systemextensionsctl list |
| Um host inalcançável | 2 (BSD / DNS) | dig, nslookup, dscacheutil -q host |
| Handshake lento | 5-6 (TLS / URLSession) | URLSessionTaskMetrics, unified log |
| Request nunca sai do dispositivo | 5-6 (Network.framework) | unified log com.apple.network |
| Orçamento de tráfego por app desconhecido | 4 (contadores de kernel) | nettop, ova |
| Destino suspeito | 2-3 (BSD / filtro NE) | lsof -i, content filter |
| Corrupção aleatória de body | 6+ (HTTP) | interceptor URLProtocol, captura de pacote |
| Render lento depois de resposta rápida | 8 (view) | Time Profiler do Instruments |
Alguns hábitos que pagam em todos esses:
- Sempre logue
taskMetrics. Mesmo em release. É barato e poupa horas depois. - Marque cada request de saída. Configure um header custom como
X-Request-Ide coloque nas linhas de log do cliente e do servidor. Agora você pode juntar. - Não confie no indicador de atividade. Só mostra que algum request está em vôo, não qual ou por quanto tempo está pendente.
- Faça profile a frio e a quente separadamente. Primeiro request depois do launch bate em DNS, TCP, TLS e possivelmente negociação HTTP/3. Décimo request reusa a conexão e é 10-50x mais rápido. Os dois números importam.
- Cuidado com coalescing de conexão. HTTP/2 e HTTP/3 vão reusar uma única conexão entre hosts que compartilham um certificado. Isso torna raciocínio por host mais difícil.
Encerrando
A pilha de camadas de rede do macOS é densa mas conhecível. Do socket(2) na base ao URLSession e sua camada de view no topo, cada camada oferece uma lente específica — pacotes, flows, requests, tasks, frames. Escolha a lente certa e bugs colapsam de "a rede está lenta" para "o handshake TLS para api.example.com leva 1,2 segundo porque a cadeia de certificado está sendo re-buscada toda vez".
Se quer uma visão casual e sempre ligada de qual app está usando sua banda agora — sem nettop rodando num terminal — instale o ova. É um app de barra de menu, cerca de 3 MB, roda em macOS 14 e posteriores, amostra a cerca de 1 Hz e armazena tudo localmente. Sem telemetria, sem dashboard remoto, pagamento único.