Skip to content

Ports and Connections


A network service becomes much easier to reason about once you can connect three things together: a process, a socket, and a port. Linux may have dozens of processes running, but only some of them are listening for network connections, and even then they may be listening only on specific addresses or interfaces.

This lesson builds that model and then uses two practical tools, ss and nc, to inspect and create network connections. We’ll also build a tiny Python listener so you can see exactly how a process becomes reachable through an IP address and port.

What Is A Port?

An IP address identifies a host at the IP layer.

A port identifies a network endpoint associated with a particular transport protocol on that host.

For example:

192.168.1.20:22

means:

IP address → 192.168.1.20
Port       → 22

Port 22 is conventionally used by SSH, while HTTP commonly uses 80 and HTTPS commonly uses 443.

A useful simplified model is:

IP address
    ↓
Which machine?

Port
    ↓
Which network service on that machine?

The combination is what allows multiple services to use the same IP address.

For example:

192.168.1.20:22   → SSH
192.168.1.20:80   → HTTP
192.168.1.20:443  → HTTPS

All three can coexist because the transport layer distinguishes their ports.

TCP And UDP Ports

Ports exist with transport protocols such as TCP and UDP.

That means these are conceptually different endpoints:

TCP 192.168.1.20:53
UDP 192.168.1.20:53

They can coexist because TCP and UDP maintain separate transport namespaces.

TCP is connection-oriented. A TCP connection is established between two endpoints and maintains state while the connection exists.

UDP is connectionless. Applications send individual datagrams without establishing a TCP-style connection first.

For this lesson, most of the practical demonstrations will use TCP because it makes the relationship between listening services and established connections easier to see.

What Is A Socket?

A socket is the operating system interface through which a process communicates over a network.

A simplified picture is:

Process
   │
   ▼
Socket
   │
   ▼
TCP / UDP
   │
   ▼
IP
   │
   ▼
Network Interface

When a server application listens on:

0.0.0.0:8080

it has created a socket that is prepared to accept connections on TCP port 8080 across the machine’s IPv4 interfaces.

When a client connects to it, the kernel tracks the resulting connection as another socket.

This is why looking at sockets can tell you a great deal about what the machine is doing on the network.

Listening And Established Connections

There are two states you will encounter constantly when inspecting TCP sockets.

A listening socket is waiting for incoming connections:

LISTEN

An established connection is an active TCP connection between a local endpoint and a remote endpoint:

ESTAB

Conceptually:

Server

192.168.1.20:8080
        │
        │ LISTEN
        ▼
   waiting...


Client

192.168.1.50:54321
        │
        │ connects to
        ▼
192.168.1.20:8080

        ↓

ESTAB

The client normally receives an ephemeral source port such as 54321, while the server uses its well-known or configured listening port.

ss: The Main Tool For Inspecting Connections

The command you’ll use for socket inspection is:

ss

ss stands for socket statistics.

It replaces many of the older socket-inspection patterns that administrators historically used with netstat.

The basic command:

ss

shows active socket information.

However, the real value comes from combining its options to answer specific questions.

ss Patterns And Options

This is where ss becomes genuinely useful for administration. You don’t need to memorize every option, but you should become comfortable constructing a command based on the question you’re trying to answer.

Show Listening Sockets

Start with:

ss -l

This shows listening sockets.

For TCP listeners, a more useful pattern is:

ss -lt

The options mean:

-l  listening
-t  TCP

So:

ss -lt

means:

Show TCP sockets that are listening.

You may see something like:

State   Local Address:Port
LISTEN  0.0.0.0:22
LISTEN  127.0.0.1:631
LISTEN  [::]:22

This immediately answers:

“What TCP ports are accepting connections on this machine?”

Show UDP Listeners

For UDP:

ss -lu

Here:

-l  listening
-u  UDP

Unlike TCP, UDP doesn’t establish connections in the same way, so don’t expect the same LISTEN/ESTAB lifecycle.

Show Both TCP And UDP

You can combine the protocol options:

ss -ltu

This is one of the most useful quick checks on a server.

It answers:

“What TCP and UDP sockets are currently listening?”

Show Process Information

A port number tells you that something is listening, but administrators usually want the next piece of information:

“Which process owns this socket?”

Use:

sudo ss -lntup

The important options are:

-l  listening
-n  numeric addresses and ports
-t  TCP
-u  UDP
-p  process information

You may see:

Netid State  Local Address:Port  Process
tcp   LISTEN 0.0.0.0:22         users:(("sshd",pid=812,fd=3))

The process information connects networking back to the process model.

Port
 ↓
Socket
 ↓
Process

This is extremely useful when you discover an unexpected listening port.

Note

Process information can require elevated privileges. If ss -lntup doesn’t show the process details you expect, try it with sudo.

Avoid Resolving Names

By default, some networking tools may attempt to turn addresses and ports into names.

For administration, numeric output is often preferable because it is faster and less ambiguous:

ss -n

For example:

192.168.1.20:8080

is easier to reason about than output that tries to replace addresses with hostnames.

This is especially useful during troubleshooting because you don’t want your socket-inspection command itself waiting on DNS resolution.

A common pattern is therefore:

ss -lnt

rather than:

ss -lt

when you want compact, numeric TCP listener information.

Show Established TCP Connections

To focus on active TCP connections:

ss -tn

This shows TCP sockets using numeric addresses.

To specifically show established connections:

ss -tn state established

This is useful when you want to answer:

“Which TCP connections are currently active?”

You might see:

ESTAB 192.168.1.20:22 192.168.1.50:53421
ESTAB 192.168.1.20:443 192.168.1.60:41230

Now you can see both sides of the connection.

Show Everything

A broad inspection command is:

ss -ant

This includes TCP sockets rather than only listeners.

For both TCP and UDP:

ss -anut

The exact command you choose should depend on the question you’re asking. Starting with a huge output dump and then trying to understand it is usually less useful than filtering toward the specific state or protocol you care about.

Show Listening Sockets With Numeric Output And Processes

One command worth becoming comfortable with is:

sudo ss -lntup

It gives you a practical overview of listening TCP and UDP sockets:

listen
+ numeric
+ TCP
+ UDP
+ process

This is often the first command worth running when you inherit a Linux server and want to understand what network services are exposed.

Filter By Port

You can filter socket output using expressions.

For example:

ss -lnt 'sport = :8080'

This asks for TCP listening sockets whose source port is 8080.

For a destination port:

ss -tn 'dport = :443'

The distinction between sport and dport matters when inspecting established connections.

For a listening server socket, the local port is generally the service’s listening port, so filtering on sport is often what you want.

Filter By Address

You can also filter by local address.

For example:

ss -lnt 'src 127.0.0.1'

This can help determine whether a service is listening only on loopback.

Similarly:

ss -lnt 'src 0.0.0.0'

can show IPv4 listeners bound to all local IPv4 addresses.

The exact filter syntax is more powerful than you’ll normally need, but the pattern is worth learning:

ss [display options] 'filter expression'

This lets you ask a focused question instead of manually searching through a large output.

Why 0.0.0.0 And 127.0.0.1 Matter

Consider:

127.0.0.1:8080

versus:

0.0.0.0:8080

A service bound to 127.0.0.1 accepts connections through the loopback interface only.

That means a remote machine cannot normally connect to it.

A service bound to 0.0.0.0 is listening on all local IPv4 interfaces.

For example:

127.0.0.1:8080

means roughly:

this machine only

while:

0.0.0.0:8080

means:

all local IPv4 interfaces

This distinction is one of the most common causes of confusion when a service works locally but cannot be reached from another machine.

IPv6 Listeners

You may also encounter:

[::]:8080

This is an IPv6 wildcard address.

It is the IPv6 equivalent of listening on all IPv6 interfaces.

Don’t confuse:

0.0.0.0:8080

with:

[::]:8080

They belong to different address families.

Whether an IPv6 wildcard listener also accepts IPv4 connections can depend on the application’s socket configuration and the system’s IPv6 behavior, so don’t assume that seeing [::]:8080 automatically means IPv4 clients can connect.

A Practical ss Decision Tree

Instead of memorizing random combinations, start with the question.

What is listening?
    → ss -lntup

Which TCP connections are active?
    → ss -tn state established

What is listening on port 8080?
    → ss -lnt 'sport = :8080'

Which process owns the listener?
    → sudo ss -lntup

Do I need numeric addresses?
    → add -n

The important skill is constructing the command from the information you need.

nc: Creating And Testing Connections

nc, commonly called netcat, is a small networking utility that can create TCP or UDP connections and can also listen for incoming connections.

It is useful because it lets you test the network without needing a full application server.

For example, start a TCP listener:

nc -l 8080

On another terminal, connect to it:

nc 127.0.0.1 8080

Now type something in the client terminal:

hello

The listener terminal should receive:

hello

You’ve just created a real TCP connection between two processes on the same machine.

The flow is:

nc client
127.0.0.1:ephemeral-port
        │
        │ TCP
        ▼
nc listener
127.0.0.1:8080

The exact nc options can vary slightly between implementations, so if your Debian installation behaves differently, check:

nc --help

The networking concept remains the same.

Watch The Connection With ss

The nc example becomes much more useful when you inspect it from another terminal.

Start the listener:

nc -l 8080

Then connect:

nc 127.0.0.1 8080

While the connection is active, run:

ss -tn

You should see an established connection involving port 8080.

You may see something conceptually like:

ESTAB 127.0.0.1:8080 127.0.0.1:43122
ESTAB 127.0.0.1:43122 127.0.0.1:8080

The exact output depends on the implementation and system, but the important relationship is:

Listener process
       │
       ▼
Listening socket
       │
       ▼
TCP connection
       │
       ▼
Client socket
       │
       ▼
Client process

This is the connection between the process and network layers that administrators need to understand.

Build A Tiny Python TCP Server

Now let’s replace nc with a tiny Python program.

Create:

server.py

with:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen()

print("Listening on 127.0.0.1:8080")

connection, address = server.accept()

print(f"Connection from {address}")

data = connection.recv(1024)

print(f"Received: {data.decode()}")

connection.sendall(b"Hello from the server\n")

connection.close()
server.close()

Run it:

python3 server.py

You should see:

Listening on 127.0.0.1:8080

The Python process has now created a TCP listening socket.

Check it from another terminal:

ss -lntp

You should find a listener on:

127.0.0.1:8080

and, with sufficient privileges, the process information will identify Python as the owner.

What Did The Python Program Actually Do?

The important lines are:

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

This creates an IPv4 TCP socket.

Then:

server.bind(("127.0.0.1", 8080))

associates the socket with:

127.0.0.1:8080

Then:

server.listen()

changes the socket into a listening socket.

Finally:

connection, address = server.accept()

waits for a client to connect.

The sequence is:

socket()
  ↓
bind()
  ↓
listen()
  ↓
accept()
  ↓
established connection

You don’t need to become a Python networking programmer here. The example exists to demonstrate what the operating system sees when an application opens a network service.

Connect To The Python Server

While the Python program is waiting at:

server.accept()

connect to it from another terminal:

nc 127.0.0.1 8080

The Python program should print something similar to:

Connection from ('127.0.0.1', 43122)
Received: hello

if you type:

hello

into the nc terminal.

It will then send:

Hello from the server

back to the client.

Now you can see the entire chain:

Python process
     │
     ▼
TCP socket
     │
     ▼
127.0.0.1:8080
     │
     ▼
Linux kernel
     │
     ▼
nc client

This is much closer to what a real network service does.

What Does The Client Port Mean?

You may have noticed that the client had a port such as:

43122

You did not specify that port.

The operating system selected an ephemeral port for the outgoing connection.

So the connection looks like:

Client
127.0.0.1:43122
      │
      │ TCP
      ▼
Server
127.0.0.1:8080

The server has a predictable listening port.

The client generally receives a temporary source port so that many simultaneous connections can coexist.

For example:

192.168.1.50:43122 → 192.168.1.20:8080
192.168.1.50:43123 → 192.168.1.20:8080
192.168.1.50:43124 → 192.168.1.20:8080

All three can connect to the same server port because their client-side endpoints differ.

A TCP Connection Is Identified By Endpoints

A useful simplified model for a TCP connection is:

source IP
source port
destination IP
destination port

For example:

192.168.1.50:43122
        ↓
192.168.1.20:8080

Another client can connect:

192.168.1.60:51234
        ↓
192.168.1.20:8080

Both are connections to the same server port, but they are different connections because their source endpoints differ.

This is why a server can have hundreds or thousands of simultaneous clients connected to one listening port.

Listening Port Vs Established Connection

This distinction is worth seeing directly.

Suppose Python is listening on:

127.0.0.1:8080

Before a client connects:

LISTEN
127.0.0.1:8080

After a client connects:

LISTEN
127.0.0.1:8080

ESTAB
127.0.0.1:43122
        ↕
127.0.0.1:8080

The listening socket doesn’t simply disappear when a connection arrives. The server continues listening for additional clients while established connection sockets represent individual clients.

Conceptually:

                 ┌── Client A
                 │
Listening socket ├── Client B
                 │
                 └── Client C

This is one reason ss output can contain many entries for the same local port.

Why A Service Can Work Locally But Not Remotely

Now we can explain a common Linux networking problem.

Suppose your Python server uses:

server.bind(("127.0.0.1", 8080))

On the same machine:

nc 127.0.0.1 8080

works.

But another machine cannot connect to:

192.168.1.20:8080

Why?

The service is bound only to loopback.

The listening socket is:

127.0.0.1:8080

not:

0.0.0.0:8080

If you change the example to:

server.bind(("0.0.0.0", 8080))

the service listens on all local IPv4 interfaces.

Now the machine’s actual network address can be used to reach it.

This is a very common distinction when diagnosing services.

A Practical ss Investigation

Imagine somebody reports:

“Port 8080 isn’t working.”

Don’t immediately assume the application is broken.

Start with:

sudo ss -lntp 'sport = :8080'

There are several possible outcomes.

Nothing Is Listening

If nothing is returned, there may simply be no service listening on that port.

Check the application or service that is supposed to provide it.

Something Is Listening On Loopback

You might see:

127.0.0.1:8080

The service exists, but it is reachable only locally.

Something Is Listening On All IPv4 Interfaces

You might see:

0.0.0.0:8080

Now the service is potentially reachable through the machine’s IPv4 interfaces, assuming routing and other controls allow the connection.

Something Is Listening On A Specific Address

You might see:

192.168.1.20:8080

The service is bound specifically to that address.

This can be intentional, especially on machines with multiple interfaces.

The point is that ss lets you distinguish these cases immediately.

Ports Are Not Security Boundaries

A listening port means a process has made itself available through a network socket.

It does not mean the service is necessarily reachable from every network.

For example:

Process
  ↓
Listening on 0.0.0.0:8080
  ↓
Routing / interface
  ↓
Other network controls
  ↓
Remote client

Whether a remote client can actually connect depends on the complete path.

This is why “the port is listening” and “the port is reachable” are different statements.

We will use this distinction heavily when we reach the networking debugging topic.

Useful ss Commands To Remember

You don’t need to memorize the entire ss manual. These patterns cover a large part of everyday administration:

ss -lnt

Show TCP listeners using numeric addresses and ports.

ss -lntup

Show listening TCP and UDP sockets with numeric output and process information.

ss -tn state established

Show established TCP connections.

ss -ant

Show TCP sockets, including non-listening states.

ss -lnt 'sport = :8080'

Show a TCP listener on port 8080.

sudo ss -lntup

Show listening sockets and the processes that own them.

The pattern to internalize is:

ss
│
├── what protocol?
│     ├── -t TCP
│     └── -u UDP
│
├── what state?
│     └── -l listening
│
├── how much detail?
│     ├── -n numeric
│     └── -p process
│
└── what subset?
      └── filter expression

Cleanup

If you created the Python example, stop it with:

Ctrl+C

If you started an nc listener, stop it with:

Ctrl+C

The server.py file can be removed when you’re finished:

rm server.py

What You Should Remember

A network service is not just “a port.”

The useful mental model is:

Process
   ↓
Socket
   ↓
IP address + port
   ↓
Transport protocol
   ↓
Network

ss lets you inspect the kernel’s view of those sockets.

nc gives you a lightweight way to create and test network connections.

The Python example shows how an ordinary process can create a socket, bind it to an address and port, listen for connections, and accept a client.

Most importantly, when you see something like:

0.0.0.0:8080

you should immediately think:

A process is listening on TCP port 8080 across the machine’s IPv4 interfaces.

And when you see:

127.0.0.1:8080

you should think:

The service is listening on port 8080, but only through the local loopback interface.

Those two observations alone solve a surprising number of real-world “the service is running but I can’t connect” problems.

What’s Next

We’ve now connected processes to network sockets and ports. The next topic stays short and simple ways to manage your wireless (WiFi) connection. It can be handy — the nmcli tool.

Last updated on