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

RangeNamePurpose
0 - 1023Well-known / SystemReserved for standard services. Binding requires root or CAP_NET_BIND_SERVICE
1024 - 49151RegisteredAssigned by IANA for specific applications. Can be used by regular users
49152 - 65535Dynamic / EphemeralUsed 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:

FieldMeaning
StateLISTEN = waiting for connections
Recv-QBytes in receive queue (0 is normal for LISTEN)
Send-QBacklog size (max pending connections)
Local Address:PortWhat address and port the service is bound to
Peer Address:Port* means accepting from any address
ProcessThe 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: netstat is part of the net-tools package, which is considered deprecated. Modern distributions may not include it by default. Use ss instead -- it is faster, more feature-rich, and included in iproute2 which 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

StateMeaningCommon Issues
LISTENWaiting for incoming connectionsNormal for servers
ESTABLISHEDActive connection, data flowingNormal
SYN_SENTClient sent SYN, waiting for responseFirewall blocking, server down
SYN_RECEIVEDServer received SYN, sent SYN-ACKSYN flood attack
TIME_WAITConnection closed, waiting before reuseToo many = port exhaustion
CLOSE_WAITRemote side closed, local has not yetApplication bug (not closing sockets)
FIN_WAIT_1Sent FIN, waiting for ACKRemote side not responding
FIN_WAIT_2Received ACK for our FINRemote side has not sent FIN yet
LAST_ACKSent FIN, waiting for final ACKUnusual

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
StateMeaning
openPort is accepting connections
closedPort is reachable but no service is listening
filteredA 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

  1. Port Discovery: Run ss -tlnp on your system. For each listening port, identify the service and whether it is listening on all interfaces or just localhost.

  2. Connection Tracking: Open a web browser and load several pages. Use ss -tn to observe the TCP connections being created to remote servers. Note the source (ephemeral) ports and destination ports.

  3. 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 -l to watch the TIME_WAIT accumulation.

  4. nmap Scan: Scan your own machine with nmap localhost. Compare the results with ss -tlnp. Are there any differences? Why might there be?

  5. Service Fingerprinting: Use nmap -sV to detect the version of services running on your machine. How much information does it reveal? Consider the security implications.

  6. Bonus Challenge: Set up a simple TCP echo server using nc or Python. Connect to it from another terminal, send data, and use tcpdump to capture the full TCP lifecycle: three-way handshake, data transfer, and four-way teardown. Identify each packet's flags.