TCP/IP, Ports, and Sockets¶
What You'll Learn¶
- How IPv4 addresses and CIDR blocks work, and how to size subnets
- How a Linux host decides where to send a packet, and what NAT changes
- How TCP connections open, close, and linger — and when UDP is used instead
- How ports, sockets, ephemeral ports, and MTU cause real production problems
The Layers That Matter¶
| Layer | What it does | Examples | You debug it with |
|---|---|---|---|
| Application (7) | The protocol your software speaks | HTTP, DNS, SSH, PostgreSQL | curl, dig, application logs |
| Transport (4) | Connections and ports between processes | TCP, UDP, QUIC | ss, nc, tcpdump |
| Network (3) | Addressing and routing between networks | IPv4, IPv6, ICMP | ip route, ping, mtr |
| Link (2) | Delivery on the local network | Ethernet, ARP, VLANs | ip link, ip neigh |
"Layer 4" and "Layer 7" load balancers take their names from this model.
IP Addresses and CIDR¶
An IPv4 address is 32 bits, written as four octets: 10.0.2.15. A CIDR suffix says how many leading bits are the network; the rest identify hosts.
| CIDR | Addresses | Typical use |
|---|---|---|
/32 |
1 | A single host in a firewall rule |
/28 |
16 | A small subnet |
/24 |
256 | A common subnet size |
/20 |
4,096 | A larger subnet, such as for Kubernetes pods |
/16 |
65,536 | A whole VPC |
In a traditional subnet, the first address (network) and last (broadcast) aren't usable by hosts. AWS reserves five addresses in every subnet, so a /24 gives 251 usable IPs.
Private address ranges¶
| Range | CIDR | Notes |
|---|---|---|
10.0.0.0 – 10.255.255.255 |
10.0.0.0/8 |
Most VPCs and data centers |
172.16.0.0 – 172.31.255.255 |
172.16.0.0/12 |
Docker's default bridge uses part of this |
192.168.0.0 – 192.168.255.255 |
192.168.0.0/16 |
Home and office networks |
100.64.0.0 – 100.127.255.255 |
100.64.0.0/10 |
Carrier-grade NAT; sometimes used for extra pod IPs |
Plan ranges so they never overlap across VPCs, offices, and VPNs you might connect later. Overlapping CIDRs can't be routed to each other without NAT.
Check a plan before you build it. The planner flags overlaps between your ranges and with the defaults Docker, Kubernetes distributions, and clouds pick silently:
CIDR planner (needs JavaScript). Without it, ipcalc shows each range, and Python's ipaddress.ip_network(a).overlaps(ip_network(b)) checks a pair.
ipcalc 10.0.16.0/20 # network, broadcast, host range (sudo apt install ipcalc)
ip -4 addr show # addresses on this host
Routing¶
Every host has a routing table. For each packet, the kernel picks the most specific matching route.
$ ip route
default via 10.0.0.1 dev eth0 proto dhcp src 10.0.2.15 metric 100
10.0.0.0/16 dev eth0 proto kernel scope link src 10.0.2.15
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
$ ip route get 8.8.8.8
8.8.8.8 via 10.0.0.1 dev eth0 src 10.0.2.15
- Destinations inside
10.0.0.0/16go directly outeth0. - Everything else goes to the default gateway
10.0.0.1. ip route getshows exactly which route and source address a destination uses — the fastest way to answer "why is traffic leaving through that interface?"
On the local segment, the host learns the gateway's MAC address with ARP:
NAT¶
Network address translation rewrites addresses as packets cross a boundary.
| Type | Rewrites | Example |
|---|---|---|
| Source NAT (SNAT, masquerade) | The source address of outgoing traffic | Private instances reaching the internet through a NAT gateway; containers through the Docker bridge |
| Destination NAT (DNAT, port forwarding) | The destination address of incoming traffic | docker run -p 8080:80; Kubernetes Services via kube-proxy |
Two consequences you'll meet in practice:
- Servers behind SNAT all appear to the outside world as the NAT's public IP. Allow-listing that IP allows the whole private network.
- A NAT device tracks every connection. Exhausting its connection table or source ports causes intermittent timeouts under load.
NAT idle timeouts¶
A NAT device forgets a connection that's been idle too long. The next packet on it gets a reset, and the application sees Connection reset by peer on a connection it thought was healthy. Linux only sends TCP keepalives after 7,200 seconds idle by default, far longer than any of these:
| Device | Idle timeout for TCP |
|---|---|
| AWS NAT gateway | 350 seconds, fixed |
| AWS Network Load Balancer | 350 seconds by default, configurable 60–6,000 since 2024 |
| Azure NAT gateway | 4 minutes by default, configurable to 120 |
| Google Cloud NAT | 1,200 seconds by default (established connections) |
Long-lived idle connections, such as database pools and message consumers, need keepalives shorter than the smallest timeout on the path: set tcp_keepalive_time below 300 seconds for those sockets (most drivers have an option), or have the pool close idle connections first.
TCP vs UDP¶
| TCP | UDP | |
|---|---|---|
| Connection | Established with a handshake | None — independent datagrams |
| Delivery | Reliable, ordered, retransmitted | Best effort; may drop or reorder |
| Flow and congestion control | Yes | No (the application decides) |
| Used by | HTTP/1.1 and HTTP/2, SSH, databases | DNS queries, metrics (StatsD), video, QUIC and HTTP/3 |
The three-way handshake¶
sequenceDiagram
participant C as Client 10.0.2.15:51844
participant S as Server 10.0.3.20:5432
C->>S: SYN
S-->>C: SYN-ACK
C->>S: ACK
Note over C,S: ESTABLISHED — data flows
C->>S: FIN
S-->>C: ACK, then FIN
C->>S: ACK
Note over C: TIME-WAIT, 60 seconds on Linux (up to 4 minutes elsewhere)
What the client sees when the handshake fails tells you a lot:
| Symptom | Meaning | Likely cause |
|---|---|---|
Connection refused (RST reply) |
The host is reachable, but nothing is listening on that port | Service down, wrong port, listening on 127.0.0.1 only |
| Connection timed out (no reply) | Packets are silently dropped | Firewall or security group, wrong route, host down |
| No route to host | The network layer can't deliver it | Missing route, or an ICMP reject from a firewall |
Connection states¶
| State | Seen on | Worth investigating when |
|---|---|---|
LISTEN |
Servers | The expected port isn't listed |
SYN-SENT |
Clients | Many — the destination isn't answering |
ESTABLISHED |
Both | Growing without bound — a connection leak |
TIME-WAIT |
The side that closed first | Tens of thousands on a client — no connection reuse |
CLOSE-WAIT |
The side that hasn't closed yet | Growing — the application never closes sockets (a bug) |
Ports and Sockets¶
A socket is identified by five values: protocol, source IP, source port, destination IP, and destination port. That's why one server port (:443) can hold thousands of connections at once — each has a different client address or port.
sudo ss -tulpn # listening TCP and UDP sockets, with the owning process
sudo ss -tnp dst 10.0.3.20:5432 # connections from this host to the database
nc -vz 10.0.3.20 5432 # can I open a TCP connection?
Listening on 127.0.0.1:8080 accepts connections only from the same host. Listening on 0.0.0.0:8080 (or [::]:8080) accepts them on every interface — check which one you need.
Ephemeral ports¶
The client side of each connection uses a temporary port:
That's about 28,000 ports per destination IP and port. A service opening a new connection for every request to the same backend, with each socket lingering in TIME-WAIT, can run out — showing up as Cannot assign requested address errors. The fix is connection pooling and keep-alive, not kernel tuning.
Estimate how close a client is to running out:
Ephemeral port budget (needs JavaScript). Without it: ports in use per destination ≈ new connections per second × (seconds open + 60 seconds of TIME-WAIT). Compare that with the size of ip_local_port_range, 28,232 by default.
MTU¶
The maximum transmission unit is the largest packet an interface sends without fragmentation: usually 1500 bytes on Ethernet, and 9001 inside some cloud networks.
When a path has a smaller MTU than the endpoints assume (VPNs, tunnels, overlay networks) and the ICMP messages that report it are blocked, small requests work but large ones hang. This is an MTU black hole.
ip link show eth0 | grep -o 'mtu [0-9]*'
# Test a full-size packet with "don't fragment": 1472 + 28 bytes of headers = 1500
ping -M do -s 1472 -c 3 10.0.3.20
If -s 1472 fails but -s 1372 works, something on the path has a lower MTU.
A Note on IPv6¶
IPv6 uses 128-bit addresses (2001:db8::10), has no NAT in the usual design, and is increasingly the default in cloud and Kubernetes environments. The same tools work with -6 (ip -6 route, ping -6, ss shows both). When something works over IPv4 but not by hostname, check whether the name resolves to an IPv6 address the network doesn't route.
What Most Engineers Miss¶
- Two CIDR blocks never partly overlap. Either they're disjoint or one contains the other, because both are aligned powers of two. That makes overlap checks simple, and it's why "just shift the range a bit" never works.
- Docker takes far more address space than
docker0. New bridge networks, including one per Compose project, come from172.17.0.0/16through172.31.0.0/16in /16s, then192.168.0.0/16in /20s, and Swarm overlays come from10.0.0.0/8. A Compose project on a laptop can silently shadow the corporate VPN's172.20.0.0/16. Setdefault-address-poolsin/etc/docker/daemon.json. - TIME-WAIT belongs to whoever closes first. If the server closes the connection, the TIME-WAIT socket is on the server, costing it memory but no client ports. HTTP clients that close after every request pay the full 60 seconds per connection.
- A source port only has to be unique per destination.
connect()can reuse the same local port for different destination addresses or ports, so spreading traffic across backends raises the limit. Callingbind()beforeconnect()(to choose a source IP) reserves the port for all destinations at once, unless the socket setsIP_BIND_ADDRESS_NO_PORT. - AWS NAT gateways have a per-destination limit. Each IP address on a NAT gateway supports up to 55,000 simultaneous connections to each unique destination; beyond that the
ErrorPortAllocationmetric climbs and new connections fail. Add secondary IPs or spread destinations. - NAT gateways don't reassemble fragments. AWS NAT gateways don't support IP fragmentation for TCP or ICMP, so
ping -s 10000fails behind one, and so does any server that answers with fragmented TCP.
Real incident: SACK Panic (2019)
A flaw in Linux's handling of TCP selective acknowledgments (CVE-2019-11477) let a remote peer crash servers with crafted packets carrying a very small maximum segment size. Every kernel since 2.6.29 was affected (advisory). It's a reminder that the TCP stack is code that needs patching like any other, and that knowing what SACK and MSS are turns a scary advisory into a one-line mitigation (sysctl -w net.ipv4.tcp_sack=0) while you patch.
Common Mistakes¶
- Choosing overlapping VPC or office CIDRs, then being unable to connect them with peering or VPN.
- Sizing Kubernetes or container subnets too small, and running out of IP addresses as the cluster grows.
- Treating "connection refused" and "connection timed out" as the same problem — one is a missing listener, the other a firewall or routing issue.
- Binding a service to
127.0.0.1and wondering why other hosts can't reach it. - Raising the ephemeral port range instead of fixing missing connection reuse.
- Blocking all ICMP, which breaks path MTU discovery and creates hard-to-diagnose hangs.
Check Yourself¶
Resets after a quiet night
Every morning, the first query from each app server to a database outside the VPC fails with Connection reset by peer. Traffic goes through an AWS NAT gateway. What's happening?
- The database restarts overnight
- The NAT gateway dropped connections idle for over 350 seconds, and the pool reused them
- The ephemeral port range is exhausted
Enable TCP keepalives below 350 seconds on the pool's sockets, or have the pool validate or retire idle connections.
Docker breaks the VPN
After docker compose up, a developer can no longer reach 172.20.4.10 on the office VPN. Why?
- Compose created a bridge network in
172.20.0.0/16, and the local route to it wins over the VPN - Docker's iptables rules block all VPN traffic
- The VPN doesn't support containers
Docker hands out /16s from 172.17.0.0/16 to 172.31.0.0/16. Pin default-address-pools to a range nothing else uses.
Interview Questions¶
- How many usable IP addresses are in a
/26subnet in AWS, and why? - What's the difference between "connection refused" and "connection timed out"?
- Walk through the TCP three-way handshake. What is
TIME-WAIT, and why does it exist? - A service logs thousands of
CLOSE-WAITsockets. What does that suggest? - What is an MTU black hole, and how would you detect one?
Next¶
Continue to DNS.