TCP, UDP & Ports
Why This Matters
A web application is "slow." Users complain about timeouts. You SSH into the server, and everything looks fine -- CPU is idle, memory is available, disk I/O is normal. You check the web server, and it is running. But then you look at the network connections:
ss -s
Total: 12847
TCP: 12583 (estab 2034, closed 9200, orphaned 0, timewait 9150)
Over 9,000 connections stuck in TIME_WAIT. The server is running out of ephemeral ports. New connections cannot be established because there are no free source ports available.
Understanding TCP, UDP, connection states, and port management is not optional for anyone running production services. The transport layer is where your applications actually talk to each other, and when it breaks, knowing these fundamentals is the difference between a five-minute fix and hours of fumbling.
Try This Right Now
# See all listening TCP ports on your system
ss -tlnp
# See all established connections
ss -tnp
# Count connections by state
ss -s
# See what process is using a specific port
ss -tlnp | grep ':22'
# Check a well-known port mapping
grep -w "80" /etc/services
These commands will make much more sense by the end of this chapter.
TCP vs UDP: Two Transport Protocols
The transport layer (Layer 4) has two main protocols. They solve the same problem -- getting data between applications -- but they make very different trade-offs.
TCP: Transmission Control Protocol
TCP is connection-oriented and reliable. Before any data is sent, TCP establishes a connection. It guarantees that data arrives in order, without duplicates, and retransmits anything that gets lost.
Characteristics:
- Connection-oriented (three-way handshake)
- Reliable delivery (acknowledgments, retransmissions)
- Ordered (data arrives in the sequence it was sent)
- Flow control (sender slows down if receiver is overwhelmed)
- Congestion control (sender slows down if the network is congested)
- Higher overhead (headers, state tracking, retransmissions)
Used by: HTTP/HTTPS, SSH, FTP, SMTP, database connections, anything where losing data is unacceptable.
UDP: User Datagram Protocol
UDP is connectionless and unreliable (in the technical sense -- it does not guarantee delivery). There is no handshake, no acknowledgment, no retransmission. You send a packet and hope it arrives.
Characteristics:
- Connectionless (no handshake, just send)
- Unreliable (no acknowledgment, no retransmission)
- Unordered (packets can arrive in any order)
- No flow control or congestion control
- Lower overhead (minimal header, no state)
- Faster for real-time applications
Used by: DNS (queries), DHCP, NTP, streaming video/audio, online gaming, VoIP, VPN tunnels (WireGuard, OpenVPN in UDP mode).
Side-by-Side Comparison
+-----------------------------+-----------------------------+
| TCP | UDP |
+-----------------------------+-----------------------------+
| Connection-oriented | Connectionless |
| Reliable delivery | Best-effort delivery |
| Ordered | Unordered |
| Retransmits lost data | Lost data stays lost |
| Flow control | No flow control |
| Higher latency | Lower latency |
| 20-60 byte header | 8 byte header |
| Streaming data (byte stream)| Message-based (datagrams) |
+-----------------------------+-----------------------------+
| Used for: | Used for: |
| Web (HTTP/HTTPS) | DNS queries |
| Email (SMTP/IMAP) | Video streaming |
| SSH | Online gaming |
| File transfer | VoIP (phone calls) |
| Database connections | DHCP |
+-----------------------------+-----------------------------+
Think About It: Why does DNS typically use UDP instead of TCP? DNS queries are small (usually fit in a single packet), and the speed of UDP matters when you resolve dozens of names per page load. However, DNS DOES fall back to TCP for large responses (like zone transfers) or when a response is truncated.
The TCP Three-Way Handshake
Before any data flows over a TCP connection, the two sides perform a three-way handshake to establish the connection. This is one of the most important concepts in networking.
Client Server
| |
| 1. SYN (seq=100) |
| "I want to connect" |
|--------------------------------->|
| |
| 2. SYN-ACK (seq=300, ack=101) |
| "OK, I acknowledge your SYN" |
|<---------------------------------|
| |
| 3. ACK (seq=101, ack=301) |
| "Got it, connection established"|
|--------------------------------->|
| |
| Connection ESTABLISHED |
| Data can now flow both ways |
| |
Step 1 - SYN: The client sends a SYN (synchronize) packet with a random initial sequence number. "I want to start a conversation."
Step 2 - SYN-ACK: The server responds with its own SYN and an ACK (acknowledgment) of the client's SYN. "I hear you, and I want to talk too."
Step 3 - ACK: The client acknowledges the server's SYN. "Great, let's go."
After this, the connection is ESTABLISHED and data flows.
Connection Teardown: Four-Way Handshake
Closing a TCP connection takes four steps (or sometimes three, with a combined FIN-ACK):
Client Server
| |
| 1. FIN |
| "I'm done sending" |
|--------------------------------->|
| |
| 2. ACK |
| "OK, noted" |
|<---------------------------------|
| |
| 3. FIN |
| "I'm done too" |
|<---------------------------------|
| |
| 4. ACK |
| "Got it, connection closed" |
|--------------------------------->|
| |
| Connection CLOSED |
| |
Hands-On: Watching the Handshake
You can actually see the three-way handshake using tcpdump:
# In terminal 1: start capturing on the loopback interface
sudo tcpdump -i lo -nn port 8080
# In terminal 2: start a simple listener
nc -l -p 8080
# In terminal 3: connect to it
nc localhost 8080
In the tcpdump output, you will see something like:
10:00:01 IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [S], seq 12345
10:00:01 IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [S.], seq 67890, ack 12346
10:00:01 IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [.], ack 67891
[S] = SYN, [S.] = SYN-ACK, [.] = ACK. You just witnessed the three-way handshake.
Port Numbers
A port number is a 16-bit integer (0-65535) that identifies a specific application or service on a host. Think of the IP address as a street address and the port as the apartment number.
IP Address : Port
192.168.1.100 : 443
| |
"Which server" "Which service on that server"
Port Ranges
| Range | Name | Purpose |
|---|---|---|
| 0 - 1023 | Well-known / System | Reserved for standard services. Binding requires root or CAP_NET_BIND_SERVICE |
| 1024 - 49151 | Registered | Assigned by IANA for specific applications. Can be used by regular users |
| 49152 - 65535 | Dynamic / Ephemeral | Used by the OS for outgoing connections (source ports) |
Common Well-Known Ports
+--------+------------------+------------+
| Port | Service | Protocol |
+--------+------------------+------------+
| 20/21 | FTP (data/ctrl) | TCP |
| 22 | SSH | TCP |
| 23 | Telnet | TCP |
| 25 | SMTP | TCP |
| 53 | DNS | TCP/UDP |
| 67/68 | DHCP (srv/client)| UDP |
| 80 | HTTP | TCP |
| 110 | POP3 | TCP |
| 123 | NTP | UDP |
| 143 | IMAP | TCP |
| 443 | HTTPS | TCP |
| 465 | SMTPS | TCP |
| 514 | Syslog | UDP |
| 587 | SMTP (submission)| TCP |
| 993 | IMAPS | TCP |
| 995 | POP3S | TCP |
| 3306 | MySQL | TCP |
| 5432 | PostgreSQL | TCP |
| 6379 | Redis | TCP |
| 8080 | HTTP (alt) | TCP |
| 8443 | HTTPS (alt) | TCP |
+--------+------------------+------------+
The /etc/services File
Linux maintains a mapping of port numbers to service names:
# Search for a port number
grep -w "443" /etc/services
# Search for a service name
grep -w "ssh" /etc/services
# Count total entries
wc -l /etc/services
Sample output:
https 443/tcp
https 443/udp
ssh 22/tcp
Ephemeral Ports
When your machine initiates a connection (e.g., your browser connecting to a web server), the OS assigns a random source port from the ephemeral range:
# See the configured ephemeral port range
cat /proc/sys/net/ipv4/ip_local_port_range
# Output: 32768 60999
# This means the OS will use ports 32768-60999 for outgoing connections
# That's 28,232 available source ports
If you run out of ephemeral ports (too many connections, or too many stuck in TIME_WAIT), new outgoing connections will fail. This is a common production issue.
Think About It: If a server has one IP address and the ephemeral port range is 32768-60999, what is the maximum number of simultaneous outgoing connections it can make to a single destination IP and port? (Answer: 28,232. Each connection needs a unique source-IP:source-port to destination-IP:destination-port combination.)
Examining Connections with ss
The ss command (socket statistics) is the modern replacement for the older netstat. It
is faster and provides more information.
Basic ss Usage
# Show all TCP sockets
ss -t
# Show all UDP sockets
ss -u
# Show listening sockets only
ss -l
# Show process information (requires root for all processes)
ss -p
# Show numeric addresses (don't resolve hostnames)
ss -n
# Common combinations:
# All listening TCP ports with process info and numeric addresses
ss -tlnp
# All established TCP connections
ss -tnp state established
# All listening UDP ports
ss -ulnp
# Summary statistics
ss -s
Reading ss Output
ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=5678,fd=6))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1234,fd=4))
LISTEN 0 511 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=9012,fd=20))
Breaking this down:
| Field | Meaning |
|---|---|
State | LISTEN = waiting for connections |
Recv-Q | Bytes in receive queue (0 is normal for LISTEN) |
Send-Q | Backlog size (max pending connections) |
Local Address:Port | What address and port the service is bound to |
Peer Address:Port | * means accepting from any address |
Process | The process, PID, and file descriptor |
Important: 0.0.0.0:80 means listening on ALL interfaces. 127.0.0.1:3306 means
listening ONLY on localhost -- MySQL is not accessible from other machines. [::]:22 means
listening on all IPv6 addresses.
Filtering Connections
# Filter by state
ss -tn state established
ss -tn state time-wait
ss -tn state close-wait
# Filter by port
ss -tn sport = :22 # Source port 22
ss -tn dport = :443 # Destination port 443
ss -tn '( sport = :80 or sport = :443 )' # Port 80 or 443
# Filter by address
ss -tn dst 192.168.1.0/24 # Connections to a subnet
# Count connections per state
ss -tn state established | wc -l
ss -tn state time-wait | wc -l
The Older netstat (Still Useful)
# Install if not present
# Debian/Ubuntu: sudo apt install net-tools
# Fedora/RHEL: sudo dnf install net-tools
# Equivalent of ss -tlnp
netstat -tlnp
# All connections
netstat -an
# Statistics
netstat -s
Distro Note:
netstatis part of thenet-toolspackage, which is considered deprecated. Modern distributions may not include it by default. Usessinstead -- it is faster, more feature-rich, and included iniproute2which is installed everywhere.
TCP Connection States
A TCP connection goes through several states during its lifetime. Understanding these states is critical for debugging connection issues.
+------------------------------------------------------------------+
| TCP CONNECTION STATE DIAGRAM |
+------------------------------------------------------------------+
| |
| Client Server |
| |
| CLOSED CLOSED |
| | | |
| | connect() | listen() |
| v v |
| SYN_SENT ----SYN----> LISTEN |
| | | |
| | <----SYN-ACK---- | |
| v v |
| ESTABLISHED ----ACK----> SYN_RECEIVED |
| | | |
| | v |
| | ESTABLISHED |
| | | |
| | (data transfer) | |
| | | |
| | close() | |
| v | |
| FIN_WAIT_1 ----FIN----> | |
| | v |
| | <----ACK---- CLOSE_WAIT |
| v | |
| FIN_WAIT_2 | close() |
| | v |
| | <----FIN---- LAST_ACK |
| v | |
| TIME_WAIT ----ACK----> CLOSED |
| | |
| | (wait 2*MSL) |
| v |
| CLOSED |
| |
+------------------------------------------------------------------+
States You Need to Know
| State | Meaning | Common Issues |
|---|---|---|
LISTEN | Waiting for incoming connections | Normal for servers |
ESTABLISHED | Active connection, data flowing | Normal |
SYN_SENT | Client sent SYN, waiting for response | Firewall blocking, server down |
SYN_RECEIVED | Server received SYN, sent SYN-ACK | SYN flood attack |
TIME_WAIT | Connection closed, waiting before reuse | Too many = port exhaustion |
CLOSE_WAIT | Remote side closed, local has not yet | Application bug (not closing sockets) |
FIN_WAIT_1 | Sent FIN, waiting for ACK | Remote side not responding |
FIN_WAIT_2 | Received ACK for our FIN | Remote side has not sent FIN yet |
LAST_ACK | Sent FIN, waiting for final ACK | Unusual |
Diagnosing State Problems
Too many TIME_WAIT:
# Count TIME_WAIT connections
ss -tn state time-wait | wc -l
# If thousands: connections are being opened and closed rapidly
# TIME_WAIT lasts for 2*MSL (Maximum Segment Lifetime), typically 60 seconds
# Kernel tuning options (use with caution):
# Allow reuse of TIME_WAIT sockets
cat /proc/sys/net/ipv4/tcp_tw_reuse
# 1 = enabled (safe for clients, helps with outgoing connections)
Too many CLOSE_WAIT:
# Count CLOSE_WAIT connections
ss -tn state close-wait | wc -l
# CLOSE_WAIT means the remote end closed the connection, but YOUR application
# has not called close() on the socket. This is almost always an application bug.
# Find which process has them:
ss -tnp state close-wait
WARNING: Do not blindly tune kernel TCP parameters to "fix" TIME_WAIT or other state issues. These parameters exist for good reasons (preventing old duplicate packets from being accepted by new connections). Instead, fix the root cause -- typically an application that opens too many short-lived connections.
Practical Port Scanning with nmap
nmap (Network Mapper) is the standard open-source tool for network exploration and port
scanning. It helps you discover what services are running and what ports are open.
# Install nmap
# Debian/Ubuntu
sudo apt install nmap
# Fedora/RHEL
sudo dnf install nmap
# Arch
sudo pacman -S nmap
WARNING: Only scan networks and systems you own or have explicit permission to scan. Unauthorized port scanning can violate laws and policies. In many jurisdictions, scanning someone else's network without permission is illegal.
Basic Scans
# Scan common ports on a host
nmap 192.168.1.1
# Scan specific ports
nmap -p 22,80,443 192.168.1.1
# Scan a port range
nmap -p 1-1024 192.168.1.1
# Scan all 65535 ports
nmap -p- 192.168.1.1
# Scan a subnet
nmap 192.168.1.0/24
# UDP scan (requires root)
sudo nmap -sU -p 53,67,123 192.168.1.1
# Service version detection
nmap -sV -p 22,80,443 192.168.1.1
# OS detection (requires root)
sudo nmap -O 192.168.1.1
Reading nmap Output
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 192.168.1.1
Host is up (0.0010s latency).
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
3306/tcp closed mysql
8080/tcp filtered http-proxy
Nmap done: 1 IP address (1 host up) scanned in 0.25 seconds
| State | Meaning |
|---|---|
open | Port is accepting connections |
closed | Port is reachable but no service is listening |
filtered | A firewall is blocking nmap's probe packets |
Hands-On: Scan Your Own Machine
# Scan your localhost
nmap localhost
# Compare with ss output
ss -tlnp
# They should show the same open ports
# nmap shows them from the "outside" perspective
# ss shows them from the "inside" perspective
Hands-On: Putting It All Together
Let us create a practical exercise that demonstrates TCP connections, ports, and state transitions:
# Terminal 1: Start a TCP listener on port 9999
nc -l -p 9999
# Terminal 2: Watch the connection states
watch -n 0.5 'ss -tn | grep 9999'
# Terminal 3: Connect to the listener
nc localhost 9999
# In Terminal 2, you should see:
# ESTAB 0 0 127.0.0.1:random_port 127.0.0.1:9999
# ESTAB 0 0 127.0.0.1:9999 127.0.0.1:random_port
# Type some text in Terminal 3 -- it appears in Terminal 1
# This is TCP at work: reliable, ordered delivery
# Press Ctrl-C in Terminal 3 to close the connection
# Watch Terminal 2 -- you may briefly see TIME_WAIT
# Now try UDP:
# Terminal 1:
nc -u -l -p 9999
# Terminal 3:
nc -u localhost 9999
# Type text -- it arrives, but there is no connection state to track
# UDP is connectionless
Debug This
A developer reports that their application cannot connect to the database. They get a "Connection refused" error on port 5432.
# Step 1: Is PostgreSQL listening?
ss -tlnp | grep 5432
# Output:
# LISTEN 0 128 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=1234,fd=5))
PostgreSQL is listening, but only on 127.0.0.1. If the application is on a different
server, it cannot connect because PostgreSQL is not listening on the external interface.
Fix: Edit postgresql.conf and change listen_addresses:
# From:
listen_addresses = 'localhost'
# To:
listen_addresses = '*' # Listen on all interfaces
# Or:
listen_addresses = '0.0.0.0' # Listen on all IPv4 interfaces
Then restart PostgreSQL and verify:
sudo systemctl restart postgresql
ss -tlnp | grep 5432
# Should now show 0.0.0.0:5432 instead of 127.0.0.1:5432
Also check pg_hba.conf to ensure the remote host is allowed to authenticate, and verify
no firewall is blocking port 5432.
What Just Happened?
+------------------------------------------------------------------+
| CHAPTER RECAP |
+------------------------------------------------------------------+
| |
| TCP: Reliable, ordered, connection-oriented. Used for web, |
| email, SSH, databases. Three-way handshake: SYN, SYN-ACK, ACK. |
| |
| UDP: Fast, connectionless, best-effort. Used for DNS, video, |
| VoIP, gaming. No handshake, no guarantees. |
| |
| PORTS: 0-1023 (well-known), 1024-49151 (registered), |
| 49152-65535 (ephemeral/dynamic). |
| |
| Key commands: |
| ss -tlnp Show listening TCP ports |
| ss -tn Show TCP connections |
| ss -s Connection statistics |
| nmap host Scan for open ports |
| |
| Connection states to watch: ESTABLISHED (normal), |
| TIME_WAIT (too many = exhaustion), CLOSE_WAIT (app bug). |
| |
| 0.0.0.0 = all interfaces. 127.0.0.1 = localhost only. |
| |
+------------------------------------------------------------------+
Try This
-
Port Discovery: Run
ss -tlnpon your system. For each listening port, identify the service and whether it is listening on all interfaces or just localhost. -
Connection Tracking: Open a web browser and load several pages. Use
ss -tnto observe the TCP connections being created to remote servers. Note the source (ephemeral) ports and destination ports. -
State Observation: Write a small script that opens and closes 100 TCP connections to a local service rapidly. Use
ss -tn state time-wait | wc -lto watch the TIME_WAIT accumulation. -
nmap Scan: Scan your own machine with
nmap localhost. Compare the results withss -tlnp. Are there any differences? Why might there be? -
Service Fingerprinting: Use
nmap -sVto detect the version of services running on your machine. How much information does it reveal? Consider the security implications. -
Bonus Challenge: Set up a simple TCP echo server using
ncor Python. Connect to it from another terminal, send data, and usetcpdumpto capture the full TCP lifecycle: three-way handshake, data transfer, and four-way teardown. Identify each packet's flags.