Voltar ao blog
·12 min de leitura·productdevbook

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:

  1. Hardware e drivers de kernel — famílias IOKit como IO80211Family para Wi-Fi, IOEthernetFamily para ethernet.
  2. Pilha de rede BSD — IP, TCP, UDP, o buffer do socket, pf para filtragem de pacote.
  3. Network Extensions — filtros de conteúdo, packet tunnel providers, DNS proxies, a nova maquinaria nw_path.
  4. syscall socket(2) — a API mais baixa do userspace; raramente chamada diretamente hoje.
  5. CFNetwork / Network.framework — a API moderna em C/Swift para conexões, paths e listeners.
  6. NSURLSession (URLSession) — o cliente HTTP/HTTPS do Foundation. O padrão para quase todo app.
  7. Bibliotecas de nível de app — Alamofire, AFNetworking, gRPC-Swift, bibliotecas WebSocket, Apollo, qualquer coisa envolvendo URLSession.
  8. 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 escutando
  • netstat -rn — tabela de roteamento
  • pfctl -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?"
  • dtrace e dtruss — tracing em nível de syscall (exige SIP off para scripts não assinados)
  • Template Network do Instruments — embrulha probes do dtrace para 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á:

  • NWConnection para uma conexão de saída (TCP, UDP, QUIC, TLS, protocolos custom)
  • NWListener para aceitar conexões de entrada
  • NWPathMonitor para observar disponibilidade de path (Wi-Fi vs celular vs ethernet)
  • NWBrowser para 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:

  • URLSessionDelegate e URLSessionTaskDelegate — veem cada redirect, desafio e completion
  • URLSessionTaskMetrics — detalhamento de timing por task: DNS, connect, secure-connect, request, response
  • URLSessionConfiguration.protocolClasses — registre uma subclasse de NSURLProtocol para logar cada request em teste
  • os_log com subsystem com.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.

Baixar para macOS

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. OperationQueue com maxConcurrentOperationCount = 1 vai 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 URLSession
  • com.apple.network — eventos do Network.framework
  • com.apple.networkd — eventos do daemon de rede
  • com.apple.cfnetwork.NSURLConnection — logs legacy do NSURLConnection

Lendo o log de rede ao vivo:

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

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

SintomaCamada provávelPrimeira ferramenta
App não alcança nenhum host2-3 (BSD / NEs)ping, traceroute, systemextensionsctl list
Um host inalcançável2 (BSD / DNS)dig, nslookup, dscacheutil -q host
Handshake lento5-6 (TLS / URLSession)URLSessionTaskMetrics, unified log
Request nunca sai do dispositivo5-6 (Network.framework)unified log com.apple.network
Orçamento de tráfego por app desconhecido4 (contadores de kernel)nettop, ova
Destino suspeito2-3 (BSD / filtro NE)lsof -i, content filter
Corrupção aleatória de body6+ (HTTP)interceptor URLProtocol, captura de pacote
Render lento depois de resposta rápida8 (view)Time Profiler do Instruments
Visão de banda por app
ova lê contadores de kernel na camada do socket, agrupa PIDs auxiliares sob o app pai e te dá uma taxa ao vivo na barra de menu mais um histórico navegável — útil quando "isso é mesmo rede?" é a primeira pergunta a responder.

Alguns hábitos que pagam em todos esses:

  1. Sempre logue taskMetrics. Mesmo em release. É barato e poupa horas depois.
  2. Marque cada request de saída. Configure um header custom como X-Request-Id e coloque nas linhas de log do cliente e do servidor. Agora você pode juntar.
  3. 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.
  4. 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.
  5. 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.