Understanding the BSD Divergence
macOS inherits its networking stack from FreeBSD, not Linux. That means `netstat`, `ifconfig`, `arp`, and `route` behave differently from their Linux counterparts. The flags overlap but are not identical. If you come from a Debian or RHEL background and type `netstat -tulpn` on macOS, you get an error because `-p` on macOS BSD netstat prints the protocol name, not the PID.
Apple has also been quietly deprecating some BSD tools in favor of their own replacements. `ifconfig` still works as of macOS 15, but Apple recommends `networksetup` and `scutil` for scripting. Neither `ip` nor `ss` ship with macOS - you either install them via Homebrew or you use the native alternatives.
The practical split is this: for interactive troubleshooting, the BSD tools work fine once you learn the flag differences. For automation and scripting in a DevOps context, lean on `networksetup`, `scutil`, and `system_profiler`, which are stable across macOS versions and return structured output.
# Check macOS and Darwin version
sw_vers
uname -r
# Example output on macOS 15.2:
# ProductName: macOS
# ProductVersion: 15.2
# BuildVersion: 24C101
# Kernel: 24.2.0
ifconfig vs ip: Adapter Information
macOS has no `ip` command natively. The BSD `ifconfig` covers interface listing, address assignment, and link state. The output format differs from Linux `ip addr`, but the information is equivalent.
To list all interfaces: `ifconfig -a`. To get a specific interface: `ifconfig en0`. On Apple Silicon Macs, `en0` is typically Wi-Fi and `en1` is the Thunderbolt Ethernet adapter when connected. On Intel Macs the mapping can differ - always verify with `networksetup -listallhardwareports`.
One practical difference: macOS `ifconfig` shows the `status: active` or `status: inactive` field for physical link state. Linux `ip link` uses `UP` and `LOWER_UP` flags. When diagnosing a cable issue on macOS, check for `status: active` on the Ethernet interface rather than looking for `LOWER_UP`.
For Wi-Fi specifically, `airport` is a hidden but powerful utility that most sysadmins miss. It lives at `/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport` and reports signal strength, BSSID, channel, and security type. We alias it in `.zshrc` for convenience.
# List all interfaces with addresses
ifconfig -a
# Show only IPv4 addresses
ifconfig -a inet
# Show only IPv6 addresses
ifconfig -a inet6
# Hardware ports mapped to interface names
networksetup -listallhardwareports
# Wi-Fi signal and connection info
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I
# Alias for convenience
alias airport='/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
airport -I | grep -E 'SSID|BSSID|agrCtlRSSI|channel'
netstat: Connections, Routing, and Sockets
BSD `netstat` on macOS supports most of the same conceptual queries as Linux, but the flags differ enough that you need a quick reference burned into memory.
To list listening ports, the macOS equivalent of `netstat -tulpn` is `netstat -an -p tcp | grep LISTEN` combined with `lsof` for PIDs. There is no single flag to show PID with socket on macOS `netstat` - that job falls to `lsof -i` or `lsof -iTCP -sTCP:LISTEN`.
The routing table on macOS is shown with `netstat -rn`. The output includes a `Flags` column: `U` means up, `G` means gateway, `H` means host route. The default route entry will show `UGSc` on most configurations. The `c` flag means the route was cloned from a network route.
Network interface statistics per-interface are available with `netstat -I en0` for a single interface or `netstat -i` for all interfaces. This gives you packet counts, error counts, and collision counts - useful for spotting a flapping NIC or a bad cable causing CRC errors.
# All TCP connections (established + listening)
netstat -an -p tcp
# All UDP sockets
netstat -an -p udp
# Listening TCP ports only
netstat -an -p tcp | grep LISTEN
# Routing table (numeric)
netstat -rn
# Interface statistics
netstat -i
# Per-interface stats for en0, refreshed every 2 seconds
netstat -I en0 2
# Network protocol summary (packets in/out per protocol)
netstat -s | head -60
lsof -i: The PID-to-Port Mapper
On macOS, `lsof -i` is the primary tool for answering 'what process is listening on port 8080?' - a question that `ss -tulpn` answers on Linux. The syntax is slightly verbose but consistent.
`lsof -i :8080` lists all processes with any socket involving port 8080. Add `-sTCP:LISTEN` to filter listening-only. Add `-n` to skip DNS resolution and `-P` to skip port name resolution, which speeds up output significantly on systems with slow DNS.
In our experience, `lsof -i` output on a busy development Mac can take 3-5 seconds without `-n -P`. With those flags it runs in under a second. Build the habit of using them.
`lsof` also handles Unix domain sockets with `-U`, which is useful when debugging inter-process communication for services like Docker Desktop or database daemons that communicate via socket files rather than TCP. To find what process owns `/tmp/mysql.sock`: `lsof /tmp/mysql.sock`.
# What is listening on port 443?
lsof -i :443 -n -P -sTCP:LISTEN
# All network connections for a specific PID
lsof -i -n -P -p 1234
# All listening TCP ports with process names
lsof -iTCP -sTCP:LISTEN -n -P
# All UDP sockets
lsof -iUDP -n -P
# Connections from a specific remote IP
lsof -i @192.168.1.50 -n -P
# Who owns a Unix socket file
lsof /var/run/docker.sock
nettop: The Real-Time Traffic Monitor
`nettop` is macOS-only and ships with the OS. It gives you per-process, per-connection bandwidth usage in real time - the closest thing macOS has to `nethogs` on Linux, but more capable in several respects. It shows bytes in and out per connection, packets per second, and connection state, all updated live.
Run it with `nettop -m tcp` for TCP only, `-m udp` for UDP only, or `nettop -m route` to monitor routing table changes in real time. For scripting, `nettop -x -c -P -L 5 -m tcp` outputs 5 samples in CSV format to stdout, which you can pipe to a file or parse with `awk`.
We used `nettop` during a production issue where a CI runner on a Mac Mini was consuming unexpected bandwidth. Running `nettop -m tcp -P -c -x -L 1` and grepping for byte counts identified a Java process downloading artifact caches on every build. The fix took 10 minutes; finding it with `top` or Activity Monitor would have taken much longer.
For teams running automated build infrastructure on macOS, tools like those at taskbotshub.ai can complement `nettop` data collection by triggering alerts or build pipeline actions based on bandwidth thresholds captured from `nettop` CSV output.
# Interactive real-time TCP monitor
nettop -m tcp
# UDP only
nettop -m udp
# Non-interactive: 5 CSV samples to stdout
nettop -x -c -P -L 5 -m tcp
# Filter to a specific process by PID
nettop -p 1234 -m tcp
# Monitor routing changes
nettop -m route
# Save 10 samples to a file for later analysis
nettop -x -c -P -L 10 -m tcp > /tmp/nettop_$(date +%Y%m%d_%H%M%S).csv
scutil: DNS, Proxy, and Network Configuration State
`scutil` (System Configuration Utility) is the proper interface to the macOS System Configuration framework. On Linux you would read `/etc/resolv.conf` and `/etc/nsswitch.conf`. On macOS, DNS configuration is managed by the `mDNSResponder` daemon and queried through `scutil`.
`scutil --dns` shows the complete DNS resolver configuration, including per-domain resolvers, VPN-injected search domains, and mDNS settings. This is the authoritative source. Reading `/etc/resolv.conf` on macOS gives you a stub that often does not reflect the full picture, especially when VPN is active.
`scutil --proxy` shows current proxy settings from System Settings. `scutil --nwi` shows the current network interface order and primary interface. When debugging connectivity through a VPN, `scutil --nwi` quickly confirms which interface is primary and what the current DNS configuration is.
`scutil` also exposes the `ComputerName`, `LocalHostName`, and `HostName` - three separate names on macOS that often confuse people. `LocalHostName` is the Bonjour name (`.local`), `HostName` is the Unix hostname used in terminal prompts and returned by `hostname`.
# Full DNS resolver configuration
scutil --dns
# Active proxy settings
scutil --proxy
# Network interface order and primary interface
scutil --nwi
# Get computer name, local hostname, hostname
scutil --get ComputerName
scutil --get LocalHostName
scutil --get HostName
# Set hostname (requires sudo)
sudo scutil --set HostName myserver.example.com
sudo scutil --set ComputerName myserver
sudo scutil --set LocalHostName myserver
# Interactive mode to explore the config store
scutil
> show State:/Network/Global/DNS
> show State:/Network/Global/IPv4
> quit
networksetup: Scripting Network Configuration
`networksetup` is the command-line interface to the same settings found in System Settings > Network. It is the correct tool for scripting network changes on macOS - more stable than writing to preference files directly, and it respects system configuration locks.
Common uses: switching DNS servers for a specific interface, toggling Wi-Fi, configuring proxy settings, and listing available networks. On managed Macs in a DevOps fleet, `networksetup` commands are often wrapped in shell scripts or Ansible tasks.
One subtlety: `networksetup` uses the service name (like `Wi-Fi` or `Ethernet`) not the BSD interface name (`en0`). Get service names with `networksetup -listallnetworkservices`. The names are case-sensitive and can contain spaces, so quote them in scripts.
Setting DNS servers for a specific interface requires the exact service name and then a space-separated list of DNS server IPs. To revert to DHCP-assigned DNS, use `networksetup -setdnsservers "Wi-Fi" empty`.
# List all network services
networksetup -listallnetworkservices
# Get current DNS for Wi-Fi
networksetup -getdnsservers Wi-Fi
# Set DNS servers for Wi-Fi
sudo networksetup -setdnsservers Wi-Fi 1.1.1.1 1.0.0.1
# Revert to DHCP DNS
sudo networksetup -setdnsservers Wi-Fi empty
# Get IP address info for Ethernet
networksetup -getinfo Ethernet
# Enable/disable Wi-Fi
networksetup -setairportpower en0 off
networksetup -setairportpower en0 on
# List available Wi-Fi networks
networksetup -listpreferredwirelessnetworks en0
# Configure HTTP proxy
sudo networksetup -setwebproxy Wi-Fi 127.0.0.1 8888
sudo networksetup -setwebproxystate Wi-Fi off
ping, traceroute, and mtr
macOS `ping` defaults to sending packets indefinitely (like Linux), but the flood ping option (`-f`) requires root on macOS and sends packets as fast as possible without waiting for replies. Useful for packet loss testing on a local segment. `ping -c 100 -i 0.2 192.168.1.1` sends 100 pings at 200ms intervals and gives you a clean loss percentage.
macOS `traceroute` uses UDP probes by default (like BSD), not ICMP. If you want ICMP, use `traceroute -I`. For TCP traceroute equivalent, use `tcptraceroute` from Homebrew. When ICMP is rate-limited by a firewall but TCP port 80 passes through, `tcptraceroute` will show you hops that plain `traceroute` reports as `* * *`.
`mtr` is not included with macOS but installs cleanly via Homebrew as `brew install mtr`. It requires sudo or the binary to be setuid for ICMP mode. On macOS 15, Homebrew's `mtr` works correctly with `sudo mtr --report --report-cycles 100 8.8.8.8` to generate a 100-cycle report with loss and latency statistics per hop.
Apple ships `traceroute6` and `ping6` as separate binaries, but both `ping` and `traceroute` accept `-6` to force IPv6 on macOS 14 and later.
# 100 pings, 200ms interval, show summary
ping -c 100 -i 0.2 8.8.8.8
# Flood ping (requires root)
sudo ping -f -c 1000 192.168.1.1
# ICMP traceroute (not UDP)
traceroute -I 8.8.8.8
# IPv6 traceroute
traceroute -6 ipv6.google.com
# TCP traceroute to port 443 (Homebrew)
brew install tcptraceroute
sudo tcptraceroute 8.8.8.8 443
# mtr 100-cycle report
brew install mtr
sudo mtr --report --report-cycles 100 8.8.8.8
# mtr continuous interactive
sudo mtr 8.8.8.8
dig, host, and DNS Debugging
macOS ships with `dig` and `host` from BIND, typically version 9.10.x on older macOS and version 9.18.x on macOS 15. Check with `dig -v`. The commands work identically to Linux.
The critical macOS-specific DNS tool is `dscacheutil`, which flushes and inspects the DNS cache maintained by `mDNSResponder`. To flush DNS cache: `sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder`. The `killall -HUP mDNSResponder` is necessary in addition to the flush - the flush clears the local cache but the signal tells the daemon to reload. Both lines are required.
`dscacheutil -q host -a name example.com` queries the DNS cache for a hostname. Note this queries the OS resolver cache, not a DNS server directly - useful for seeing what macOS has cached versus what `dig` returns from the server.
For diagnosing split-horizon DNS failures, compare `dig @8.8.8.8 example.com` (direct to Google DNS, bypassing system resolver) with `dscacheutil -q host -a name example.com` (what the OS resolver returns). If they differ, a VPN or corporate DNS resolver is intercepting queries.
When you're setting up internal infrastructure and registering external domain names for project hostnames, services like nicename.me can help identify clean, available names before you configure split-horizon DNS entries.
# Check dig version
dig -v
# Standard query
dig example.com A
# Query specific DNS server
dig @1.1.1.1 example.com MX
# Reverse lookup
dig -x 8.8.8.8
# Flush DNS cache (macOS)
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
# Query OS DNS cache
dscacheutil -q host -a name github.com
# Check what the system resolver returns vs direct DNS
dig @8.8.8.8 internal.example.com
dscacheutil -q host -a name internal.example.com
# DNS trace (follow delegation chain)
dig +trace example.com
tcpdump and Packet Capture
`tcpdump` on macOS uses libpcap and behaves identically to Linux for the common capture flags. Syntax for capture filters is the same BPF syntax. The main differences are interface naming and the presence of `pktap` pseudo-interfaces unique to macOS.
Capture all interfaces at once: `tcpdump -i any` works on macOS, but uses a `pktap` interface underneath. The output includes a `recv if:` annotation on each packet showing which physical interface received it - useful when debugging routing between multiple interfaces.
For VPN troubleshooting, capture on the VPN tunnel interface directly. `ifconfig` will show the `utun0`, `utun1`, etc. interfaces. `tcpdump -i utun0 -n -w /tmp/vpn.pcap` captures the decrypted traffic inside the tunnel.
macOS also has `tcpdump` access controls. On macOS 14 and 15, you need to either run as root or add your user to the `access_bpf` group: `sudo dseditgroup -o edit -a $(whoami) -t user access_bpf`. After that, `tcpdump` works without `sudo`, which matters for development workflows where passwordless packet capture is needed.
For GUI analysis, `Wireshark` installs cleanly on macOS via the `.dmg` or `brew install --cask wireshark`. It integrates with the `ChmodBPF` helper to handle permissions automatically.
# Capture on Wi-Fi interface, no DNS resolution
sudo tcpdump -i en0 -n
# Capture to file for Wireshark
sudo tcpdump -i en0 -w /tmp/capture.pcap
# Filter: only HTTP traffic
sudo tcpdump -i en0 -n port 80 or port 443
# Capture on VPN tunnel
sudo tcpdump -i utun0 -n -w /tmp/vpn.pcap
# All interfaces at once
sudo tcpdump -i any -n
# Add user to access_bpf group (one-time setup)
sudo dseditgroup -o edit -a $(whoami) -t user access_bpf
# Log out and back in, then:
tcpdump -i en0 -n port 53
# Show available interfaces for capture
tcpdump -D
nc, curl, and Connectivity Testing
macOS ships with OpenBSD `nc` (netcat), which differs from the traditional netcat and from ncat (Nmap's version). The OpenBSD `nc` does not support the `-e` flag for executing a command on connection. The flags for listening mode, UDP mode, and timeout are slightly different from Linux.
`nc -zv host port` for port scanning still works on macOS. For testing TCP connectivity to a host:port combination without sending data: `nc -z -w 3 192.168.1.10 5432` exits with code 0 on success and 1 on failure, making it scriptable for health checks.
macOS `curl` is typically the Apple-maintained version, often older than the latest upstream. On macOS 15, it ships as curl 8.7.1. If you need HTTP/3, QUIC, or newer TLS features, install curl via Homebrew: `brew install curl`. Homebrew's curl will be in `/usr/local/bin/curl` (Intel) or `/opt/homebrew/bin/curl` (Apple Silicon) and will shadow the system curl if Homebrew's bin is earlier in `$PATH`.
For quick connectivity testing in scripts, combine `nc` and `curl` with `/dev/tcp` bash built-in: `echo > /dev/tcp/host/port` works in bash but not in zsh (macOS default shell). In zsh scripts, stick with `nc -z`.
# Test TCP port reachability (3 second timeout)
nc -z -w 3 192.168.1.10 5432 && echo open || echo closed
# Listen on port 9999 (simple server)
nc -l 9999
# Send UDP packet
echo 'test' | nc -u 192.168.1.10 514
# Port scan range
nc -z -v 192.168.1.10 20-25
# Check curl version
curl --version
# Install newer curl via Homebrew
brew install curl
# HTTP timing breakdown
curl -o /dev/null -s -w \
'DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTotal: %{time_total}s\n' \
https://example.com
# Follow redirects, show headers
curl -L -I https://example.com
arp and Neighbor Discovery
macOS `arp` follows BSD conventions. `arp -a` shows the ARP table. `arp -d 192.168.1.1` deletes an ARP entry (requires root). To send a gratuitous ARP or perform ARP scanning, you need `arping` from Homebrew since macOS does not include it.
For IPv6 neighbor discovery, macOS uses `ndp` (Neighbor Discovery Protocol) instead of `arp`. `ndp -a` shows the IPv6 neighbor table, equivalent to `ip -6 neigh show` on Linux. `ndp -d fe80::1%en0` deletes a specific IPv6 neighbor entry.
A common production scenario: after a network change or failover, stale ARP entries on macOS clients cause connectivity failures for 20 minutes until the default ARP timeout expires. Clear them with `arp -a -d` (requires root and deletes all entries) or target specific entries. Combine with a `ping` to the new IP to immediately populate a fresh ARP entry.
# Show ARP table
arp -a
# Delete single ARP entry
sudo arp -d 192.168.1.1
# Delete all ARP entries (clears entire cache)
sudo arp -a -d
# Show IPv6 neighbor table
ndp -a
# Resolve ARP for a specific IP
arp 192.168.1.1
# arping (Homebrew required)
brew install arping
sudo arping -I en0 -c 3 192.168.1.1
# After failover: clear stale ARP and repopulate
sudo arp -d 192.168.1.100
ping -c 1 192.168.1.100
arp -n 192.168.1.100
system_profiler and Hardware-Level Network Info
`system_profiler SPNetworkDataType` dumps complete network hardware and configuration data in structured text. With `-json` flag it outputs JSON, which is useful for automation. This tool shows MAC addresses, link speed, duplex settings, and driver information that `ifconfig` does not expose.
For NIC-level statistics including errors, dropped packets, and hardware offload capabilities, use `ethtool` from Homebrew or read directly from the kernel: `sysctl net.link.generic.system` exposes some network statistics. For detailed per-interface kernel statistics: `netstat -I en0 -b` shows byte counts in addition to packet counts.
Checking link speed on macOS Ethernet: `system_profiler SPNetworkDataType | grep -A2 -i speed` or use the `networksetup -getMedia Ethernet` command which returns current and active media settings including speed and duplex.
# Full network hardware profile
system_profiler SPNetworkDataType
# JSON output for scripting
system_profiler SPNetworkDataType -json
# Just speed and duplex
networksetup -getMedia Ethernet
# Interface stats with byte counts
netstat -I en0 -b
# Kernel network sysctls
sysctl -a | grep net.inet
# Check NIC driver and hardware details
system_profiler SPNetworkDataType | grep -A 10 'Ethernet'