Capas de red en macOS: guía para desarrolladores
Un recorrido orientado a desarrolladores por la pila de red de macOS: desde socket(2) hasta Network.framework, con notas sobre observabilidad en cada capa.
- Developer tools
- macOS
- Networking
- Deep dive
Estás depurando una llamada lenta a una API. Los logs del servidor dicen que la respuesta salió en 80 ms. El usuario dice que la página tardó cuatro segundos. En algún punto del stack entre el cable y la interfaz, el tiempo desapareció, y "la red" es una respuesta demasiado amplia para ser útil. Para saber dónde mirar, necesitas un mapa mental de las capas de red de macOS, desde la llamada al socket BSD abajo hasta el framework que tu app realmente usa arriba.
Este es un recorrido para desarrolladores de cómo un paquete se convierte en un URLResponse en macOS, dónde registra e instrumenta cada capa, y qué herramienta agarras a cada nivel. Es largo porque el stack está en capas por buenas razones, y vale la pena entenderlas todas.
Las capas de red de macOS, de arriba a abajo
De abajo a arriba, el stack de red de macOS se ve aproximadamente así:
- Hardware y drivers del kernel — familias IOKit como
IO80211Familypara Wi-Fi,IOEthernetFamilypara ethernet. - Stack de red BSD — IP, TCP, UDP, el buffer de socket,
pfpara filtrado de paquetes. - Network Extensions — filtros de contenido, proveedores de túnel de paquetes, proxies DNS, la nueva maquinaria
nw_path. - syscall
socket(2)— la API de espacio de usuario más baja; rara vez se llama directamente ya. - CFNetwork / Network.framework — la API moderna C/Swift para conexiones, rutas y listeners.
- NSURLSession (
URLSession) — el cliente HTTP/HTTPS de Foundation. El predeterminado para casi cada app. - Librerías a nivel de app — Alamofire, AFNetworking, gRPC-Swift, librerías WebSocket, Apollo, cualquier cosa envolviendo
URLSession. - Tu capa de vista — vistas SwiftUI, UIKit, AppKit que finalmente renderizan la respuesta.
La mayoría de los desarrolladores de apps pasan su tiempo en la capa 6-8 y tratan todo lo de abajo como "la red". Eso está bien hasta que algo se rompe. Cuando estás cazando un bug real —handshake TLS expirando, peticiones atascadas en autenticación de proxy, tráfico sospechoso de una dependencia— necesitas saber qué ofrece cada capa de red de macOS y dónde registra.
El stack inferior: BSD, Network Extensions, sockets
Las tres capas programables inferiores encajan estrechamente, así que vale la pena mirarlas como una losa.
Capa 2: el stack BSD
Cuando un paquete llega a en0, el kernel lo pasa por el stack de red BSD. Decide si el paquete es para ti (haciendo coincidir la IP de destino y el puerto con un socket), lo pasa por pf (el filtro de paquetes BSD, el firewall de macOS) y lo encola en el buffer del socket de recepción.
Esta capa es donde viven tcpdump y Wireshark. Aprovechan el dispositivo BPF (Berkeley Packet Filter), que ve los paquetes crudos antes de que el kernel haya hecho mucho con ellos.
sudo tcpdump -i en0 -n 'tcp port 443'Herramientas útiles a nivel del stack BSD:
netstat -an— sockets escuchando, conexiones establecidas, puertos en escuchanetstat -rn— tabla de enrutamientopfctl -s rules— conjunto de reglas pf actual (a menudo vacío a menos que activaras el firewall)ifconfig/networksetup -listallnetworkservices— estado de interfaces
Instrumentas aquí cuando sospechas un problema en la capa IP/TCP: tormenta de retransmisiones TCP, ruta inestable, una regla pf bloqueando tráfico.
Capa 3: Network Extensions
Las Network Extensions son cómo apps de terceros y la propia Apple enganchan la ruta de datos sin escribir kexts (que están deprecados). Las grandes categorías:
- Content Filter Providers — ven los metadatos del flujo y deciden permitir/denegar. Little Snitch y LuLu usan esto.
- Packet Tunnel Providers — túnel completo estilo VPN. Tailscale, apps WireGuard, NordVPN, etc.
- App Proxy Providers — enrutamiento por app a través de un endpoint remoto.
- DNS Proxy Providers — interceptan consultas DNS antes de la resolución.
Si tu tráfico va a algún sitio que no esperas, una Network Extension es uno de los culpables más probables. Comprueba systemextensionsctl list para extensiones activas.
Capa 4: socket(2) y compañía
La clásica API POSIX: socket(), bind(), connect(), send(), recv(), close(). Casi nunca escribes este código directamente en macOS hoy en día: Apple lo desaconseja para nuevas aplicaciones porque no se integra con nw_path para cambios de conectividad, no maneja Happy Eyeballs (stack dual IPv4/IPv6) y no obtiene TLS gratis.
Pero sigue siendo el cimiento. Cada API de nivel superior eventualmente llama a socket(). Cuando lees la atribución de procesos del kernel —a través de proc_pidinfo con PROC_PIDFDSOCKETINFO, o a través de /dev/bpf— estás leyendo el estado que las llamadas a socket configuraron.
Instrumentas aquí con:
lsof -i -P -n— cada socket abierto en el sistema, con PID y nombre de proceso- API
proc_pidinfo— la manera bendecida por Apple para preguntar "¿qué sockets tiene abiertos este PID?" dtraceydtruss— trazado a nivel de syscall (requiere SIP off para scripts sin firmar)- Plantilla de Red en
Instruments— envuelve probes dedtracepara llamadas a socket
Un monitor de ancho de banda por app como ova lee esta capa. Muestrea los contadores de bytes por PID, socket e interfaz del kernel aproximadamente a 1 Hz, atribuye los bytes de cada PID a su bundle de app padre y almacena las series temporales resultantes localmente. No hay inspección de paquetes: los bytes vienen de los mismos contadores que el kernel expone a nettop.
El stack medio: Network.framework y URLSession
La mayoría de la red a nivel de app vive en estas dos APIs. Comparten linaje pero resuelven problemas distintos.
Capa 5: CFNetwork y Network.framework
CFNetwork es la API C más antigua. Network.framework (introducida en 2018, amigable con Swift) es su reemplazo moderno y lo que Apple recomienda para cualquier código nuevo de red de bajo nivel.
Network.framework te da:
NWConnectionpara una conexión saliente (TCP, UDP, QUIC, TLS, protocolos personalizados)NWListenerpara aceptar conexiones entrantesNWPathMonitorpara observar la disponibilidad de la ruta (Wi-Fi vs celular vs ethernet)NWBrowserpara descubrimiento Bonjour- Stacks TLS, proxy y de protocolo componibles como
NWProtocolFramer
Lo usas directamente cuando estás escribiendo protocolos peer-to-peer, protocolos binarios personalizados, multipath TCP, o implementando software de servidor para macOS. Para todo lo que tenga forma de HTTP, vas una capa más arriba.
Instrumentación a esta capa: NWConnection.stateUpdateHandler, NWPathMonitor para vigilar cambios de ruta, y el subsistema del log unificado com.apple.network (más sobre el log unificado abajo).
Capa 6: URLSession
Si tu app habla HTTP, casi seguro usas URLSession. Maneja HTTP/1.1, HTTP/2, HTTP/3 (QUIC) donde se soporta, TLS, redirecciones, caché, cookies y desafíos de autenticación. También se integra con NSURLProtocol para que puedas interceptar su tráfico para tests.
Los hooks de instrumentación más útiles aquí son:
URLSessionDelegateyURLSessionTaskDelegate— ve cada redirección, desafío y completaciónURLSessionTaskMetrics— desglose de tiempo por tarea: DNS, conexión, conexión segura, petición, respuestaURLSessionConfiguration.protocolClasses— registra una subclase deNSURLProtocolpara registrar cada petición en pruebasos_logcon subsistemacom.apple.CFNetwork— el canal de log unificado al que CFNetwork emite
URLSessionTaskMetrics está infrautilizado. Te dice exactamente cuánto tiempo se gastó en resolución DNS vs conexión TCP vs handshake TLS vs transmisión de petición vs procesamiento de servidor vs transmisión de respuesta, por tarea. Si tu problema de llamada lenta a la API es realmente un handshake lento, estos son los datos que lo prueban.
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)
}
}Ve ova en acción
Un monitor de ancho de banda en la barra de menú visible de un vistazo: local, firmado, ~3 MB.
La cima del stack: librerías y la capa de vista
Las dos capas más altas son donde la mayoría del código de app pasa su tiempo. Los bugs aquí son fáciles de confundir con problemas de red.
Capa 7: librerías a nivel de app
La mayoría de las apps envuelven URLSession en algo como Alamofire o un módulo de red personalizado. Estas librerías normalmente exponen una señal de petición-completada que puedes enganchar para logging. También suelen tener su propia lógica de reintentos, throttling y colas que pueden enmascarar problemas de capas inferiores.
Al depurar:
- Comprueba si la librería está haciendo reintentos automáticos: tres reintentos pueden convertir una petición fallida en cuatro líneas en el log y cuatro veces los bytes en el cable.
- Comprueba la configuración de timeouts. Los predeterminados a menudo son sorprendentes (60s para el timeout de recursos de
URLSession). - Comprueba paralelismo oculto.
OperationQueueconmaxConcurrentOperationCount = 1serializará peticiones de maneras que podrías no esperar.
Capa 8: la capa de vista
A veces el bug no está en la red en absoluto. La respuesta llegó en 80 ms pero la vista tardó 3,9 segundos en renderizar porque alguien metió un decode JSON síncrono en el hilo principal, o el pase de layout cascadeó por 800 celdas. El Time Profiler de Instruments es la herramienta correcta para esto: te mostrará si el tiempo de hilo principal se gastó en JSONDecoder.decode o en UIView.layoutSubviews.
La lección: no culpes a la red hasta que la hayas medido. URLSessionTaskMetrics más una traza del Time Profiler resuelven la mayoría de los debates de "página lenta" en 10 minutos.
El log unificado
Cortando a través de cada capa está el log unificado (os_log / Logger). Los subsistemas relacionados con red incluyen:
com.apple.CFNetwork— eventos a nivel URLSessioncom.apple.network— eventos de Network.frameworkcom.apple.networkd— eventos del daemon de redcom.apple.cfnetwork.NSURLConnection— logs de NSURLConnection legados
Leer el log de red en vivo:
log stream --predicate 'subsystem == "com.apple.network"' --level debugLeer la última hora de eventos CFNetwork:
log show --last 1h --predicate 'subsystem == "com.apple.CFNetwork"'Esto es enormemente útil cuando el problema es "mi petición ni siquiera salió del dispositivo". El log te dirá que la ruta no se satisfizo, que el script de auto-config de proxy devolvió DIRECT, o que el certificado de servidor TLS fue rechazado con un código de razón específico.
Elegir la herramienta correcta, y unos cuantos hábitos
Una chuleta para "tengo un problema en esta capa, ¿qué herramienta?":
| Síntoma | Capa probable | Primera herramienta |
|---|---|---|
| La app no puede alcanzar ningún host | 2-3 (BSD / NEs) | ping, traceroute, systemextensionsctl list |
| Un host inalcanzable | 2 (BSD / DNS) | dig, nslookup, dscacheutil -q host |
| Handshake lento | 5-6 (TLS / URLSession) | URLSessionTaskMetrics, log unificado |
| La petición nunca sale del dispositivo | 5-6 (Network.framework) | log unificado com.apple.network |
| Presupuesto de tráfico por app desconocido | 4 (contadores del kernel) | nettop, ova |
| Destino sospechoso | 2-3 (filtro BSD / NE) | lsof -i, filtro de contenido |
| Corrupción aleatoria del cuerpo | 6+ (HTTP) | interceptor URLProtocol, captura de paquetes |
| Renderizado lento tras respuesta rápida | 8 (vista) | Time Profiler de Instruments |
Algunos hábitos que rinden a través de todos estos:
- Loguea siempre
taskMetrics. Incluso en release. Es barato y ahorra horas después. - Etiqueta cada petición saliente. Pon una cabecera personalizada como
X-Request-Idy métela en tus líneas de log de cliente y de servidor. Ahora puedes hacer join. - No confíes en el indicador de actividad. Solo muestra que alguna petición está en vuelo, no cuál o cuánto tiempo lleva pendiente.
- Perfila frío y caliente por separado. La primera petición tras el lanzamiento toca DNS, TCP, TLS y posiblemente negociación HTTP/3. La décima petición reutiliza la conexión y es 10-50x más rápida. Ambos números importan.
- Cuidado con el coalescing de conexiones. HTTP/2 y HTTP/3 reutilizarán una sola conexión entre hosts que comparten certificado. Esto hace que el razonamiento por host sea más difícil.
Para terminar
El stack de capas de red de macOS es denso pero conocible. Desde socket(2) abajo hasta URLSession y tu capa de vista arriba, cada capa ofrece una lente específica: paquetes, flujos, peticiones, tareas, frames. Elige la lente correcta y los bugs colapsan de "la red está lenta" a "el handshake TLS a api.example.com tarda 1,2 segundos porque la cadena de certificados se está volviendo a obtener cada vez".
Si quieres una vista casual y siempre activa de qué app está usando tu ancho de banda ahora mismo, sin nettop corriendo en un terminal, instala ova. Es una app de barra de menú, alrededor de 3 MB, funciona en macOS 14 y posteriores, muestrea aproximadamente a 1 Hz, y almacena todo localmente. Sin telemetría, sin panel remoto, pago único.