Kali Linux Networking for DevOps · Part 7 of 15
Linux Ports and Listening Services With ss
Series curriculum (15 lessons)
A service is only reachable if something is listening for connections on a port. That one sentence explains an enormous share of the “the app is running but I can’t connect” incidents you will ever debug. The process is alive, the container is up, the deployment is green — and yet every connection attempt fails. Nine times out of ten the answer is not “restart it”. It is a specific, checkable fact: which ports is this host actually listening on, and on which addresses? This lesson teaches you to answer that question in seconds with ss, the modern Linux socket-inspection tool, and to read the single most misunderstood field it prints: the bind address.
By the end you will be able to look at any Linux host or container, list exactly what is accepting connections, tell the difference between a socket that is waiting for clients and one that is already serving a client, and diagnose the classic gotcha where a service works locally but rejects every remote client.
What You Will Learn
- What a listening socket is, and how it differs from an established connection
- The
ss -tulpncommand and what every one of those flags means - How to read a socket line: local address, remote address, port, and process
- The critical difference between binding to
127.0.0.1,0.0.0.0, and a single interface IP - How to diagnose “the app runs but remote clients can’t connect”
- Why this matters even more inside containers and Kubernetes
Listening Sockets vs Established Connections
A port is just a 16-bit number (1–65535) that lets one host run many network services at once — 443 for HTTPS, 22 for SSH, 5432 for PostgreSQL. A socket is the combination of an IP address and a port that a program has claimed.
There are two states you care about day to day:
- A listening socket is a service waiting for new clients on a port. Nothing has connected yet; it is holding the door open. A web server sitting idle still has a listening socket on port 443.
- An established connection is an active conversation between a local socket and a remote socket — a client that has actually connected and is exchanging data.
LISTENING server waits, no client yet
:443 <-- door open, nobody through it
ESTABLISHED a client is connected right now
server:443 <--> client:51234
When you debug “can’t connect”, you almost always want the listening view first: is anything holding the door open on the port I expect, and on an address my client can reach? That is exactly what ss shows.
The Core Command: ss -tulpn
ss (socket statistics) is the standard tool for inspecting sockets on modern Linux. Kali ships it by default. The command you will type most often is:
ss -tulpn
Read the flags one at a time — each is a filter or a formatting choice, and understanding them means you can adjust the command instead of memorizing it:
-t— show TCP sockets (connection-oriented: HTTP, SSH, databases).-u— show UDP sockets (connectionless: DNS, syslog, some VPNs).-l— show only listening sockets. Drop this flag and you also see established connections.-p— show the process (program name and PID) that owns each socket. This usually needssudoto reveal processes you do not own.-n— numeric output: print:443instead of resolving it to:https, and print raw IPs instead of doing reverse DNS. Numeric output is faster and, more importantly, unambiguous — you see exactly what the kernel sees.
So ss -tulpn reads as: “Show me every TCP and UDP socket that is listening, with the owning process, in numeric form.” That is your standard “what is this box serving?” command.
A common variant drops UDP when you only care about TCP services:
ss -lntp
Same idea — listening, numeric, tcp, process — just narrower. The letters can appear in any order; ss -tulpn and ss -ntulp are identical.
🔎 Troubleshooting Tip — Memorize one mnemonic: “ss tulpn” — listening TCP and UDP ports, numeric, with processes. It is the first command to run on any host where a service “isn’t answering.”
Reading a Socket Line
Here is a single listening line from ss -tulpn output:
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 0 4096 0.0.0.0:443 0.0.0.0:*
Walk across it field by field, because each column answers a real question:
- Netid
tcp— the protocol. This is a TCP socket (you would seeudpfor UDP services). - State
LISTEN— it is waiting for clients. For established connections this column readsESTABinstead. - Recv-Q / Send-Q — receive and send queue sizes. On a listening socket,
Send-Q(here4096) is the backlog: how many pending connections the kernel will queue. ARecv-Qthat climbs on a listening socket means the app is accepting connections slower than they arrive. - Local Address:Port
0.0.0.0:443— the address and port this host is listening on. This is the field the rest of the lesson is about. - Peer Address:Port
0.0.0.0:*— the remote end. For a listening socket there is no peer yet, so it shows the wildcard*. On an established connection this is the actual client’s IP and port.
So this line tells us: a TCP service is listening on port 443, on all interfaces, ready for any client. What does that mean, and why does the exact local address decide whether remote clients can reach you? That is the key lesson.
The Key Lesson: Bind Address Decides Who Can Connect
When a service starts, it must bind to an address — it tells the kernel which network interface(s) it will accept connections on. That choice, visible in the Local Address column, is one of the most common and most confusing gotchas in all of infrastructure work. The port number is often correct; the bind address is what breaks the connection.
There are three cases you must be able to tell apart at a glance:
127.0.0.1:8080 loopback only
-> local processes on THIS host only
-> remote clients: REFUSED
0.0.0.0:8080 all interfaces (wildcard)
-> reachable from anywhere the
network/firewall allows
10.20.30.40:8080 one specific interface
-> reachable ONLY via that IP;
other interfaces: REFUSED
Let’s make each concrete:
127.0.0.1:8080— loopback only.127.0.0.1is the loopback address; traffic to it never leaves the machine. A service bound here can be reached by other processes on the same host (andcurl localhost:8080works), but any remote client is refused. This is a safe default for internal-only services — a database that only the local app should touch, or an admin endpoint.0.0.0.0:8080— all interfaces.0.0.0.0is a wildcard meaning “every IPv4 interface on this host.” A service bound here accepts connections from anywhere the network path and firewall allow. This is what you want for a service that is supposed to receive remote traffic — a public web server, an API other machines call.10.20.30.40:8080— one specific interface. Binding to a single real IP restricts the service to that one interface. Clients reaching the host by any other address get refused. This is used to expose a service on, say, a private management network but not the public one.
🏭 Why This Matters in Production — A service on
127.0.0.1behind a reverse proxy is a deliberate, secure pattern: only the local nginx can reach the app, and nginx handles TLS and the public interface. The same127.0.0.1binding with no proxy in front is an outage — nothing external can connect. Same socket, opposite meaning. Always ask: is this bind address intentional?
Scenario: “The App Is Running, but Remote Clients Can’t Connect”
This is the archetypal case, and it is worth walking through as a full diagnosis rather than a guess. You deployed a service, it starts cleanly, curl localhost:8080 from inside the box works — but a colleague on another machine gets connection refused every time.
Resist the urge to restart anything. Observe first. On the server, run:
sudo ss -lntp | grep 8080
Suppose the output is:
tcp LISTEN 0 128 127.0.0.1:8080 0.0.0.0:* users:(("myapp",pid=8123,fd=6))
What does this output tell us? The service is listening — the process myapp (PID 8123) owns the socket, so “the app isn’t running” is ruled out. But the Local Address is 127.0.0.1:8080. It is bound to loopback. That is the whole story: the app can only be reached from the host itself, which is exactly why curl localhost:8080 succeeds on the box and every remote client is refused.
The evidence points to the application layer, not the network. The fix is not a firewall rule and not a restart — it is a configuration change so the app binds to 0.0.0.0 (or the specific interface you want to expose). Most servers have a config setting for this: --host 0.0.0.0, bind 0.0.0.0, listen 0.0.0.0:8080, or an env var like HOST=0.0.0.0. After changing it, validate by re-running the same ss command and confirming the line now reads 0.0.0.0:8080:
tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("myapp",pid=8140,fd=6))
Now retest from the remote client. If it still fails after the bind is correct, you have cleanly moved down the stack — the app is listening correctly, so the next suspects are a firewall or security-group rule (a timeout rather than a refused), which you can chase with the connectivity tools in the next lesson.
🔎 Troubleshooting Tip — Connection refused means you reached the host but nothing accepted on that port there — often a loopback bind or the wrong port. Connection timeout usually means a firewall or security group is silently dropping packets.
sson the server distinguishes these instantly: if the port is listening on a reachable address and clients still time out, the problem is between you and the host, not the bind.
ss Replaced the Older netstat
You will still find netstat in older runbooks and StackOverflow answers. netstat is the legacy tool; on modern distributions it is deprecated in favor of ss, which is faster and reads socket data directly from the kernel. The near-equivalent legacy command is:
netstat -tulpn
The flags mean the same things they do for ss (-t tcp, -u udp, -l listening, -p process, -n numeric), which is not a coincidence — ss deliberately mirrored them to ease the transition. On many minimal images netstat isn’t even installed (it lives in the net-tools package), so prefer ss. Recognize netstat when you see it, but reach for ss.
🛠️ DevOps Perspective — Inside a container or Kubernetes pod, the bind address rule bites even harder. A process that binds to
127.0.0.1inside a container is reachable only from within that container’s own network namespace — not from the host, not from another pod, not through a published port or a Service.docker run -p 8080:8080or a Kubernetes Service will still fail to route traffic, because the app never opened the door on an externally reachable address. Rule of thumb: any service meant to receive traffic must bind to0.0.0.0inside the container. When a “published port” or Service mysteriously refuses connections,ss -lntpinside the container is the fastest way to see that the app bound to loopback. See Kali Docker networking for how namespaces and published ports fit together.
Try It Yourself
🧪 Try It — On your Kali box (or any Linux host you own), run these in order and read each result before moving on:
sudo ss -tulpn— list everything listening. Which ports are on0.0.0.0(exposed) and which on127.0.0.1(local only)? Can you name the process behind each?- Start a throwaway loopback service:
python3 -m http.server 8080 --bind 127.0.0.1. In another terminal,ss -lntp | grep 8080— confirm it shows127.0.0.1:8080. Thencurl localhost:8080(works) but note a remote client would be refused.- Stop it and restart with
python3 -m http.server 8080 --bind 0.0.0.0. Re-runssand watch theLocal Addresschange to0.0.0.0:8080. That one flag is the entire lesson.
🔐 Security Note — Only inspect and test systems you own or have explicit permission to assess.
sson your own host is pure observation, but treat the ability to expose a port thoughtfully: binding an admin or database port to0.0.0.0on an internet-facing host is how services get compromised. Expose deliberately, default to loopback for anything internal.
Common Problems
| Symptom | Likely cause | What to check |
|---|---|---|
| Works locally, refused remotely | Bound to 127.0.0.1 | ss -lntp — is Local Address loopback? |
| Nothing listed for the port | Service not started / crashed | ss -lntp | grep PORT empty → check the process |
| Refused on the right address | Wrong port, or app bound elsewhere | Confirm the exact port in ss vs your client |
| Remote client times out (not refused) | Firewall / security group drop | Bind is fine in ss; move to network layer |
-p shows no process | Missing privileges | Re-run with sudo |
Troubleshooting Workflow
Keep the diagnosis ordered — observe before you change anything:
Application -> Port -> DNS -> Route -> Interface
^^^^
ss lives here
1. Observe: sudo ss -tulpn
2. Question: is the port LISTEN-ing at all?
3. Question: on 127.0.0.1 or 0.0.0.0?
4. If loopback + remote client -> fix bind addr
5. If 0.0.0.0 + still refused -> wrong port?
6. If 0.0.0.0 + times out -> firewall/route
7. Validate: re-run ss, retest client
ss sits squarely at the Port layer of the troubleshooting stack. Its job is to answer two questions definitively — is anything listening, and on an address my client can reach? — so you know whether to fix the application’s bind configuration or move down the stack toward routing and firewalls.
What You Learned
- A listening socket waits for clients on a port; an established connection is an active session with a remote peer. Debug “can’t connect” from the listening view first.
ss -tulpnlists listening tcp and udp sockets, numerically, with the owning process.ss -lntpis the TCP-only variant.- Read the socket line: Local Address:Port is what you bound to; Peer is the remote end (
*while only listening). - The bind address decides who can connect:
127.0.0.1= loopback only,0.0.0.0= all interfaces, a specific IP = one interface only. This is a top junior-DevOps gotcha. - “Runs locally but remote clients are refused” almost always means a
127.0.0.1bind — fix the app config, not with a restart. ssreplacednetstat;netstat -tulpnis the legacy equivalent, often not even installed on minimal images.- Inside containers and Kubernetes, bind to
0.0.0.0for any service meant to receive traffic, or published ports and Services will never route to it.
You now know what is listening and where. The natural next question is whether you can actually reach it from another machine — and how to tell “refused” from “timed out” from “no route.” That is exactly what we cover next in Part 8: Testing Connectivity, building on TCP vs UDP and leading toward validating services with Nmap.
Recommended Reading
Affiliate Disclosure: Some links on this page are affiliate links. If you purchase through one of these links, DevOps AI Toolkit may earn a commission at no additional cost to you. See our affiliate disclosure.
← Back to Kali Linux Networking for DevOps Back to Kali Linux