List Listening Ports
Problem statement
Read saved ss -tlnp output and list every listening TCP port with the program behind it, and whether it can be reached from other machines or only from the server itself. "What is listening on this box?" is the first question when a service is unreachable, when a port is "already in use", and in every security review.
ss.txt (from sudo ss -tlnp)
State Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessLISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=812,fd=6))LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=900,fd=5))LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=650,fd=3))LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=812,fd=7))LISTEN 0 4096 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=950,fd=6))LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=650,fd=4))LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=1300,fd=3))Print one line per port and program, sorted by port: the port, the program name, and all interfaces or local only. IPv4 and IPv6 listeners for the same port and program should appear once.
Expected output:
== listening ports ==22 sshd all interfaces80 nginx all interfaces5432 postgres local only6379 redis-server local only9100 node_exporter all interfacesHints
0.0.0.0:80 or [::]:22. The port is everything after the last :, which sub(/.*:/, "", port) leaves behind.Approach
Optimal: ss -tlnp with awk
Covers: what listening means, ss -tlnp flags, bind addresses 0.0.0.0 / 127.0.0.1 / [::], splitting IPv6 addresses, awk match() and substr(), sort -n -u, netstat.
A listening port is an open door. A server program asks the kernel to listen on a port, and waits for connections. Two things matter: the port number, and the address it listens on, which decides who can reach it.
can connect"]:::green ~~~ A3["this machine
can connect"]:::green end subgraph LOCAL["postgres on 127.0.0.1:5432"] direction LR L1["other machines
blocked"]:::red ~~~ L3["this machine
can connect"]:::green end ALL ~~~ LOCAL classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style ALL fill:transparent,stroke:#2563eb,stroke-width:2px style LOCAL fill:transparent,stroke:#059669,stroke-width:2px
| Bind address | Who can connect |
|---|---|
0.0.0.0 |
anyone who can reach any IPv4 address of this machine |
[::] or * |
the same, for IPv6 (and often IPv4 too) |
127.0.0.1, [::1] |
only programs on this machine |
a specific IP, like 10.0.1.5 |
only through that network card |
A database on 0.0.0.0:5432 is open to the network unless a firewall blocks it; on 127.0.0.1:5432 it is safe by design.
The ss flags. ss (socket statistics) replaced the older netstat:
| Flag | Means |
|---|---|
-t |
TCP sockets (-u for UDP) |
-l |
listening sockets only |
-n |
numbers, not names (80, not http), which is faster and clearer |
-p |
show the program; needs sudo to see other users' programs |
Splitting the port off, IPv6 included. IPv6 addresses contain colons themselves, like [::]:22 or [2001:db8::1]:443. So "split at the first :" breaks them. The port is always after the last colon. In awk, sub(/.*:/, "", port) deletes everything up to the last colon, because .* grabs as much as it can, and leaves just the port. The address is the rest.
Finding the program name. The last field looks like users:(("nginx",pid=812,fd=6)). match($0, /\(\("[^"]+"/) finds (("nginx", and substr cuts out the name between the quotes. RSTART and RLENGTH are set by match() to where the match starts and how long it is.
keep after the last colon"]):::purple P --> PORT["port 22"]:::green F --> AD["address [::]"]:::blue classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
Walking through the code. The # Setup: lines only save the sample output, so skip past them.
NR > 1skips the header. For each line, awk takes the port and address from field 4 and the program name from the end.- Addresses
0.0.0.0,[::]and*becomeall interfaces; anything starting with127.or[::1]becomeslocal only. sort -n -usorts by port and removes the duplicate lines from IPv4 and IPv6 listeners of the same program.
One more program is open to the network besides SSH and the web server: node_exporter on 9100. If that is not meant to be public, a firewall rule or --web.listen-address=127.0.0.1:9100 fixes it.
Edge cases. Without sudo, the Process column is empty for programs owned by other users. A port can be listening and still unreachable because of a firewall or a cloud security group, so test from another machine too.
# Setup: save sample ss -tlnp output in a fresh temporary folder (on a server: sudo ss -tlnp)
cd "$(mktemp -d)"
cat > ss.txt << 'OUT'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=812,fd=6))
LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=900,fd=5))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=650,fd=3))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=812,fd=7))
LISTEN 0 4096 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=950,fd=6))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=650,fd=4))
LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=1300,fd=3))
OUT
echo "== listening ports =="
awk 'NR > 1 {
port = $4; sub(/.*:/, "", port) # after the LAST colon
addr = substr($4, 1, length($4) - length(port) - 1)
name = "?"
if (match($0, /\(\("[^"]+"/)) name = substr($0, RSTART + 3, RLENGTH - 4)
if (addr == "0.0.0.0" || addr == "[::]" || addr == "*") where = "all interfaces"
else if (addr ~ /^127\./ || addr == "[::1]") where = "local only"
else where = "only " addr
printf "%-5s %-14s %s\n", port, name, where
}' ss.txt | sort -n -uRecapThe whole problem in a few lines, for the night before
- Spot it: "what is listening", "port already in use", "is this exposed"
- Idea:
sudo ss -tlnp; the port is after the last:, and0.0.0.0/[::]mean open to the network - Cost: instant
- Trap: splitting IPv6 addresses at the first colon, or forgetting
sudoso the program column is empty
Interview follow-ups
Flag any database port that is open to all interfaces.
Keep a list of database ports, like
3306 5432 6379 27017, and pass it in withawk -v db=" 3306 5432 6379 27017 ". In the main block, testindex(db, " " port " ") && where == "all interfaces"and print a warning. The spaces around each number stop543from matching5432. Run it from cron with livess -tlnpoutput, and you have a small exposure check for every server.
Frequently asked questions
sudo ss -tlnp 'sport = :8080' shows only the listener on that port, with the program and PID. sudo lsof -i :8080 and sudo fuser 8080/tcp give the same answer in other forms. Then decide: stop that program, or choose another port for yours. An old copy of your own service that did not shut down is the most common answer.
netstat -tlnp shows nearly the same table and you will see it in older guides. It comes from the net-tools package, which many new systems no longer install, while ss is part of iproute2 and is always there. ss is also much faster on busy servers with many connections. Learn ss, and recognise netstat when you meet it.
Check the path step by step. Is it bound to 0.0.0.0 and not 127.0.0.1? Is a host firewall blocking it (sudo iptables -L -n, sudo nft list ruleset or sudo ufw status)? In the cloud, does the security group or network ACL allow the port from your IP? Finally test from the server itself with curl localhost:80, then from another machine with nc -zv host 80, which tells you whether the problem is the app or the network.