Checking Open Ports and Connections
Every network service on a Linux machine — a web server, an SSH daemon, a database — announces itself by listening on a port, a numbered endpoint that the kernel uses to route incoming traffic to the right process. Knowing which ports are open, which process owns them, and which remote hosts you’re currently talking to is one of the most common diagnostic tasks in system administration: is the web server actually running? Did that firewall rule work? Is something listening on a port it shouldn’t be? This lesson covers ss, the modern tool for this job, and netstat, its older but still widely-documented predecessor.
Overview / How it works
When a program wants to accept network connections, it asks the kernel for a socket — a kernel-managed communication endpoint identified by a protocol (TCP or UDP), a local IP address and port, and, once connected, a remote IP address and port. The kernel keeps a table of every socket on the system: which process opened it, what state it’s in, and what’s on the other end. ss and netstat don’t gather this information themselves — they read it out of the kernel via the /proc/net/tcp, /proc/net/udp, and related files (or, for ss, a faster netlink interface) and format it for humans.
A TCP socket moves through a well-defined set of states over its lifetime. The ones you’ll see constantly:
- LISTEN — a process has called
bind()andlisten()on this port and is waiting for incoming connections. This is what “the server is running” looks like. - ESTABLISHED — a full TCP handshake has completed; data can flow in both directions. This is an active, in-progress conversation between two endpoints.
- TIME_WAIT — the connection has been closed but the kernel is holding the socket briefly to catch any stray, delayed packets before reusing the port. This is normal and expected to appear after a connection ends, not a sign of a leak.
- CLOSE_WAIT — the remote side closed the connection but the local application hasn’t closed its end yet. A large, growing number of these usually points to a bug in the local application not cleaning up sockets.
UDP has no handshake and no real “connection state” — a UDP socket in LISTEN-like output just means a process is bound to that port and ready to receive datagrams.
A port itself is just a 16-bit number (0–65535). Ports below 1024 are the “well-known” ports (22 for SSH, 80 for HTTP, 443 for HTTPS) and normally require root privileges to bind to; anything a regular user’s program listens on is usually above 1024. “Checking open ports” really means two different things depending on where you run the check: locally, with ss/netstat, you see what the kernel is actually doing on this machine; remotely, with a tool like nmap from another host, you see what’s reachable through the network and any firewall in between. This lesson focuses on the local view.
Syntax
ss is part of the iproute2 package and is installed by default on virtually every modern distribution. netstat comes from the older net-tools package, which some minimal installs (including recent Ubuntu server images) no longer include by default — install it with sudo apt install net-tools (or sudo dnf install net-tools on Fedora/RHEL) if it’s missing.
ss [options]
netstat [options]
The most useful options are nearly identical between the two tools:
| Option | Meaning |
|---|---|
-t |
Show TCP sockets |
-u |
Show UDP sockets |
-l |
Show only listening sockets |
-n |
Show numeric addresses/ports — skip slow DNS and /etc/services name lookups |
-p |
Show the owning process name and PID (requires sudo for sockets you don’t own) |
-a |
ss: show all sockets (listening and established). netstat: same meaning |
-r |
Show the kernel routing table instead of sockets |
-s |
Show summary statistics instead of a per-socket list |
Flags are commonly combined into one cluster, e.g. -tulnp, which is read as “TCP and UDP, listening only, numeric, with process names.”
Examples
Example 1: List every listening TCP/UDP port with its owning process
sudo ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=712,fd=13))
tcp LISTEN 0 128 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=1180,fd=21))
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=934,fd=3))
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1502,fd=6))
This is the single most useful command for “what’s running on this machine and where.” Each line is one listening socket. The Local Address:Port column tells you the binding: 127.0.0.1:3306 means MySQL only accepts connections from the same machine (a good default for a database), while 0.0.0.0:80 means nginx accepts connections on port 80 from any network interface. sudo is needed here because without it, -p can only show process details for sockets owned by your own user.
Example 2: Show only active, established connections
ss -tan state established
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.1.20:22 203.0.113.7:51422
ESTAB 0 0 192.168.1.20:443 198.51.100.9:33710
This filters out listening sockets entirely and shows only live conversations. Here you can see one active SSH session from 203.0.113.7 and one HTTPS connection from 198.51.100.9. This is the command to run when you want to know “who is actually connected to this box right now,” for example while investigating unexpected load or a suspected intrusion.
Example 3: The equivalent legacy netstat command
sudo apt install -y net-tools
sudo netstat -tulnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 1180/mysqld
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 934/sshd
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1502/nginx
udp 0 0 127.0.0.53:53 0.0.0.0:* 712/systemd-resolve
netstat prints essentially the same information in a slightly different layout. It’s still worth knowing because a huge amount of existing documentation, forum answers, and older scripts use it — but its upstream project has been unmaintained for years, and ss reads the kernel’s socket tables directly rather than parsing /proc text files, which makes it noticeably faster on a machine with thousands of open sockets. New scripts and habits should prefer ss.
How it works step by step
When you run ss -tulnp, this is what happens:
ssasks the kernel, via a netlink socket, for the list of TCP and UDP sockets currently open on the system.- The kernel returns each socket’s protocol, local address/port, remote address/port (if connected), and state.
- Because you passed
-l,ssdiscards every socket that isn’t in the LISTEN state. - Because you passed
-n, it prints raw IP addresses and port numbers instead of resolving them to hostnames or service names — resolving names means DNS lookups, which can be slow or hang if a nameserver is unreachable. - Because you passed
-p, and you ran the whole command undersudo,sscross-references each socket’s inode number against/proc/<pid>/fd/for every process to figure out which PID owns it, then prints that process’s name and PID. - The results are formatted into the aligned table you see on screen.
Common Mistakes
Mistake 1: Running without sudo and assuming the process column is complete.
ss -tulnp
Without root privileges, the kernel won’t let ss see which PID owns sockets belonging to other users — those rows show an empty Process column instead of an error, which looks like the information simply isn’t available rather than being permission-restricted. Always use sudo when you need to identify the owning process:
sudo ss -tulnp
Mistake 2: Grepping for a port number without anchoring it, and matching the wrong port.
ss -tuln | grep 80
This looks like it searches for port 80, but grep matches the literal text 80 anywhere in the line — so it also matches port 8080, 8000, or even a PID or IP address containing 80. Anchor the match to the colon and a trailing boundary, or better, let ss filter natively:
ss -tuln | grep ':80 '
ss -tuln sport = :80
Mistake 3: Confusing a LISTEN socket bound to 127.0.0.1 with one bound to 0.0.0.0. A service listening on 127.0.0.1:5432 only accepts connections that originate from the same machine — it is unreachable from the network, no matter what firewall rules exist. A service on 0.0.0.0:5432 accepts connections from any interface. Misreading the bind address is a common cause of “why can’t I connect to my database from another server” (it was never listening externally) and, in the opposite direction, of accidentally exposing a service that should have stayed local.
Best Practices
- Prefer
ssovernetstatfor anything new — it’s faster, actively maintained, and pre-installed everywherenetstatwould be. - Always add
-nwhen scripting or when you want fast, unambiguous output — name resolution can hang on a broken DNS setup and turns numeric ports into ambiguous service names. - Use
sudo ss -tulnpas your default “what’s this machine doing on the network” command before diving into anything more specific. - Check the bind address (
127.0.0.1vs0.0.0.0vs a specific interface IP), not just the port number, when deciding whether a service is actually reachable. - Use
ss -sfor a quick summary of total socket counts by state when you suspect a connection leak, rather than scrolling through a huge listing by eye. - Remember that
ss/netstatshow what the kernel is doing locally — they say nothing about whether a firewall (likeufworiptables) is actually allowing that traffic in from the network. A port can show LISTEN and still be unreachable from outside.
Practice Exercises
- On a machine you control, run
sudo ss -tulnpand identify every process listening on a TCP port. For each one, note whether it’s bound to127.0.0.1or0.0.0.0, and decide whether that binding makes sense for what the service is. - Open an SSH connection from one terminal to a remote host, then in a second terminal on that same host run
ss -tan state establishedand confirm you can see your own session’s remote IP and port in the output. - Write a short one-line command using
ss(notgrepon unfiltered output) that shows only sockets listening on port 443. Compare it against a naivegrep 443approach and think through an address that would cause the naive version to give a false match.
Summary
ssandnetstatboth read the kernel’s live socket table to show open ports and connections;ssis the modern, faster tool and should be preferred.- LISTEN means a process is waiting for incoming connections on that port; ESTABLISHED means an active two-way conversation is in progress.
-t/-upick TCP/UDP,-lfilters to listening sockets,-navoids slow DNS lookups, and-p(withsudo) reveals the owning process.- The bind address (
127.0.0.1vs0.0.0.0) determines whether a listening service is reachable only locally or from the network — always check it alongside the port number. - These tools only show the kernel’s local view; a firewall can still block traffic to a port that shows as LISTEN.
