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() and listen() 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:

  1. ss asks the kernel, via a netlink socket, for the list of TCP and UDP sockets currently open on the system.
  2. The kernel returns each socket’s protocol, local address/port, remote address/port (if connected), and state.
  3. Because you passed -l, ss discards every socket that isn’t in the LISTEN state.
  4. 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.
  5. Because you passed -p, and you ran the whole command under sudo, ss cross-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.
  6. 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 ss over netstat for anything new — it’s faster, actively maintained, and pre-installed everywhere netstat would be.
  • Always add -n when 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 -tulnp as your default “what’s this machine doing on the network” command before diving into anything more specific.
  • Check the bind address (127.0.0.1 vs 0.0.0.0 vs a specific interface IP), not just the port number, when deciding whether a service is actually reachable.
  • Use ss -s for 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/netstat show what the kernel is doing locally — they say nothing about whether a firewall (like ufw or iptables) is actually allowing that traffic in from the network. A port can show LISTEN and still be unreachable from outside.

Practice Exercises

  1. On a machine you control, run sudo ss -tulnp and identify every process listening on a TCP port. For each one, note whether it’s bound to 127.0.0.1 or 0.0.0.0, and decide whether that binding makes sense for what the service is.
  2. Open an SSH connection from one terminal to a remote host, then in a second terminal on that same host run ss -tan state established and confirm you can see your own session’s remote IP and port in the output.
  3. Write a short one-line command using ss (not grep on unfiltered output) that shows only sockets listening on port 443. Compare it against a naive grep 443 approach and think through an address that would cause the naive version to give a false match.

Summary

  • ss and netstat both read the kernel’s live socket table to show open ports and connections; ss is 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/-u pick TCP/UDP, -l filters to listening sockets, -n avoids slow DNS lookups, and -p (with sudo) reveals the owning process.
  • The bind address (127.0.0.1 vs 0.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.