Skip to content
shellmap

Find all listening ports across every network interface

List every TCP / UDP port currently accepting connections on the local host — for security audits ("what is exposed?"), firewall rule verification, troubleshooting "address already in use" errors, and inventory of running services.

How to find all listening ports across every network interface in each shell

Bashunix
ss -tlnp

`ss` is the modern replacement for `netstat` (iproute2 package, default on every modern Linux). `-t` TCP, `-l` listening only, `-n` numeric (don't resolve port → service name), `-p` show process (needs root for OTHER users' processes). UDP: `-u` instead of `-t`. Both: `-tulnp`. The `Local Address:Port` column is the bind — `0.0.0.0:80` listens on every IPv4 interface, `127.0.0.1:80` is localhost-only, `192.168.1.10:80` is one specific interface, `[::]:80` is "all IPv6".

Zshunix
ss -tlnp
Fishunix
ss -tlnp
PowerShellwindows
Get-NetTCPConnection -State Listen | Sort-Object LocalPort | Format-Table LocalAddress, LocalPort, OwningProcess, @{N="ProcessName";E={(Get-Process -Id $_.OwningProcess).ProcessName}} -AutoSize

`Get-NetTCPConnection` is Windows-only (the cmdlet ships with Windows; pwsh-on-Linux/macOS has no equivalent — fall back to `ss` via the system shell). `-State Listen` filters to listening sockets. `OwningProcess` is just the PID; the computed `ProcessName` column joins to `Get-Process`. UDP equivalent: `Get-NetUDPEndpoint`. The Linux/macOS pwsh 7+ has NO `Get-NetTCPConnection` — install `coreutils` and call `ss` or `lsof`.

cmd.exewindows
netstat -ano | findstr "LISTENING"

`netstat -ano` shows all connections with addresses + ports + owning PIDs. `-a` all (vs default = only outgoing), `-n` numeric, `-o` show PID column. `findstr "LISTENING"` filters to listening state (Windows capitalizes; pipe is case-sensitive on Windows by default). To get process name from PID: `tasklist /fi "PID eq 1234"` or pipe to `for /f`. Faster pwsh form: shell to `Get-NetTCPConnection`.

Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.

Gotchas & notes

  • **`0.0.0.0` (any IPv4) vs `::` (any IPv6) — listening on one does NOT cover the other**. Linux dual-stack (default modern kernels) BINDS `::` and auto-accepts IPv4 mapped into IPv6 (`::ffff:0.0.0.0`); but apps that explicitly bind `0.0.0.0` only listen on IPv4 — IPv6 clients get connection refused on the SAME port number. This is the #1 "but I have port 80 listening — why doesn't curl work?" trap: curl on `::1` (IPv6 localhost) gets refused while curl on `127.0.0.1` works fine. macOS / BSD default DUAL-listen: `0.0.0.0` and `::` are separate sockets, no auto-bridge. Cross-platform server config: ALWAYS configure both `0.0.0.0` and `::` (or `*` if your app accepts it). `ss -tlnp4` filters IPv4-only; `ss -tlnp6` IPv6-only — confirms which family is actually bound.
  • **`netstat` is deprecated on Linux but still installed by default on macOS / Windows / BSD**. `netstat -tlnp` (Linux) gives the same answer as `ss -tlnp` but the `net-tools` package providing `netstat` is unmaintained as of ~2015. Alpine, Fedora 30+, Arch, Ubuntu 22.04+ minimal images don't include it. Cross-platform scripts: prefer `ss` on Linux, `netstat -anv` on macOS (`-v` shows PID; macOS netstat doesn't have `-p NAME` filter — pipe to `awk`), `netstat -ano` on Windows, OR `lsof -iTCP -sTCP:LISTEN -P -n` on any POSIX-y system (`-P` no port name resolution, `-n` no DNS resolution).
  • **Listening port → owning process — different commands need different privileges**. `ss -tlnp` shows process names ONLY for processes you own (your $USER); needs root/sudo for system services. `lsof -iTCP -sTCP:LISTEN -P -n` (macOS / Linux) same privilege model. Windows: `netstat -ano` always shows PID; mapping PID → process name needs `tasklist /fi "PID eq $PID"` (no privilege restriction for name; full path needs admin). The "ghost listener" investigation pattern: `sudo ss -tlnp | grep :8080` shows `users:(("python3",pid=12345,fd=4))` — confirm exactly which process owns port 8080. If `users:` field is empty even with sudo: the process exited but the socket lingered in TIME_WAIT — wait 60s and recheck, or `ss --kill state listening sport = :8080` (root, force-close).
  • **"Address already in use" — diagnosing port conflicts**: typical cause is the previous instance of the server hasn't fully released the port (TIME_WAIT state ~60s after close). `ss -tnp | grep TIME-WAIT | grep :8080` shows zombies. Fix at app level: `SO_REUSEADDR` socket option (Python `sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)`, Node `server.listen({ port, exclusive: false })`); for systemd services: socket activation eliminates the problem entirely (systemd holds the port across service restarts). Emergency reclaim: `sudo fuser -k 8080/tcp` (Linux, kills whoever owns it), `sudo lsof -ti :8080 | xargs sudo kill -9` (portable), `Get-NetTCPConnection -LocalPort 8080 | Stop-Process -Id { $_.OwningProcess } -Force` (pwsh).
  • **Containers and pods change "listening" semantics**. A process listening on `0.0.0.0:80` INSIDE a container is listening on the CONTAINER's network namespace, NOT the host. `docker ps` shows the published-port mapping; `ss -tlnp` on the host shows `docker-proxy` listening, not the actual app. To see what the container sees: `docker exec CONTAINER ss -tlnp`. K8s: `kubectl exec POD -- ss -tlnp` from within the pod's netns. Service mesh sidecars (Istio, Linkerd) ADD their own listeners (`15001`, `15006`, etc.) — what looks like "the app listening on 8080" is actually the sidecar terminating TLS at 8080 and forwarding to the app on `127.0.0.1:5000`. Always check the netns boundary before assuming what `ss` shows is the full picture.

Related commands

Related tasks