Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Networking Fundamentals
Ports and Sockets
Transport-layer multiplexing, the 5-tuple that uniquely identifies every network connection, the Berkeley socket API, well-known port security, port exhaustion, and the performance tuning that separates a 1K-connection server from a 1M-connection server.
50 min
// CHAPTER 01
The Multiplexing Problem
One IP address, millions of simultaneous connections — how does the OS sort them out?
// REAL-WORLD SCENARIO
Your laptop has one IP address: 192.168.1.10. At this moment, you have Chrome open with 20 tabs, an SSH session to a remote server, a Zoom call, Slack downloading messages, and a background macOS update. All of these simultaneously use the single IP address. When a TCP segment arrives at 192.168.1.10 from Google's server, how does the OS know which of the 20 browser tabs it belongs to? The IP address identifies the machine, not the application or conversation. The answer is port numbers and the 5-tuple.
Port numbers are 16-bit unsigned integers (0–65,535) that form the second level of addressing at the transport layer. IP addresses identify machines on a network; port numbers identify the specific service or process on that machine. Together, an IP address and port number form a socket address — the complete endpoint identifier used by the transport layer.
Every TCP connection and every UDP flow is uniquely identified by a 5-tuple:
The 5-tuple — the kernel's demultiplexing key
(source_ip, source_port, destination_ip, destination_port, protocol)
Example — two simultaneous Chrome tabs to the same server:
Tab 1: (192.168.1.10, 54321, 142.250.80.46, 443, TCP)
Tab 2: (192.168.1.10, 54322, 142.250.80.46, 443, TCP)
Same destination — different source ports — different 5-tuples.
The kernel delivers replies to exactly the right tab via this key.
The receiving server sees the reverse 5-tuple as its connection state:
(142.250.80.46, 443, 192.168.1.10, 54321, TCP) — for tab 1
(142.250.80.46, 443, 192.168.1.10, 54322, TCP) — for tab 2
The 5-tuple is the kernel's demultiplexing key: when a segment arrives, the kernel hashes the 5-tuple and looks up the corresponding socket in its connection table. The matching socket's receive buffer receives the data, and the application thread waiting on recv() is woken. This lookup happens millions of times per second on a busy server.
🌐 One Port Handles Millions of Connections
A Google frontend server handles millions of simultaneous HTTPS connections — all to destination port 443. The server's port stays constant at 443 across every connection. The diversity comes from client source IPs and source ports. Two clients from 192.168.1.10:54321 and 10.0.0.1:54321 have different source IPs, making them different 5-tuples. Within one client, different tabs use different source ports. One server IP, one server port, unlimited unique connections. Port 65K limit is a client-side constraint, not a server-side one.
// CHAPTER 02
Port Ranges and IANA Registration
Well-known, registered, and ephemeral ranges — and why root is required for ports below 1024
// REAL-WORLD SCENARIO
IANA (Internet Assigned Numbers Authority) maintains the port number registry at www.iana.org/assignments/service-names-port-numbers. It is not a technical enforcement mechanism — the OS will bind any port to any process that has permission. But it is a global coordination mechanism: by assigning port 443 to HTTPS, every client in the world knows to try 443 for encrypted web access. Without this coordination, every web server would choose an arbitrary port and clients would have no discovery mechanism.
IANA divides the 65,535 port space into three ranges:
Port range assignments
0–1023: Well-Known Ports (System Ports)
Assigned to specific protocols by IANA.
Require root/administrator privilege to bind on Unix/Linux.
(The privilege requirement prevents an unprivileged process
from impersonating a system service like SSH or HTTP.)
Examples: HTTP=80, HTTPS=443, SSH=22, DNS=53, SMTP=25
1024–49151: Registered Ports (User Ports)
Can be registered with IANA by application vendors.
No root privilege required to bind.
Examples: MySQL=3306, PostgreSQL=5432, Redis=6379,
MongoDB=27017, Elasticsearch=9200, NATS=4222
49152–65535: Dynamic/Ephemeral Ports (Private Ports)
Not assigned. Used by the OS for client-side source ports.
When your browser connects to a server, the OS picks a
random ephemeral port for your end of the connection.
RFC 6335: IANA defines 49152–65535 as ephemeral range.
Linux default: 32768–60999 (sysctl net.ipv4.ip_local_port_range).
Why Ports 0–1023 Require Root
On Unix/Linux, binding to ports below 1024 requires the CAP_NET_BIND_SERVICE capability (effectively root). This is a security boundary: an unprivileged user who could bind port 22 could impersonate the SSH daemon, capture credentials from other users who try to connect. By requiring root for well-known ports, the system ensures only trusted services claim the ports associated with specific protocols.
Modern workarounds: authbind allows specific non-root users to bind specific ports. Setting the CAP_NET_BIND_SERVICE capability on a binary allows it to bind low ports without full root. Using a reverse proxy (nginx on port 80/443, application on port 3000) is the most common cloud-native approach — nginx runs as root briefly to bind 80/443, then drops privileges.
Viewing and tuning ports
# View port assignments
cat /etc/services | head -50 # Known port names
getent services 443 # Lookup specific port
# Check ephemeral port range
sysctl net.ipv4.ip_local_port_range # Default: 32768 60999
# Expand ephemeral range for high-connection services
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# Check what is listening
ss -tlnp # TCP listening sockets with process
ss -ulnp # UDP listening sockets
ss -tanp # All TCP connections with process
# Count connections by state
ss -tan | awk '{print $1}' | sort | uniq -c
# Check TIME_WAIT accumulation
ss -tan state time-wait | wc -l
// CHAPTER 03
Well-Known Ports — A Security Reference
Every open port is an attack surface — know what you're exposing and why
// REAL-WORLD SCENARIO
A security engineer audits a newly deployed server. She runs ss -tlnp and finds ports 21 (FTP), 23 (Telnet), 3389 (RDP), and 5900 (VNC) open on public interfaces. Any single one of these would be a critical finding: FTP and Telnet send credentials in cleartext; RDP and VNC directly on the internet are prime ransomware entry points. The firewall had been disabled "temporarily" for testing. That test had been running for 11 days. Knowing what each port means — and its exact security profile — is not academic knowledge, it's operational survival.
WELL-KNOWN PORT REFERENCE
Click any port for description and security notes. Filter by category.
Click a port number to inspect its purpose and security implications
Security Posture: Attack Surface by Port
Internet-connected services on well-known ports receive automated probes within minutes of exposure. Shodan, Censys, ZoomEye, and custom botnets constantly scan the entire IPv4 address space. A server appearing on the internet with port 22 open will receive SSH brute-force attempts within 2 minutes. Port 3389 (RDP) receives credential stuffing attacks within seconds. Port 6379 (Redis, no auth) has been exploited en masse — attackers write SSH keys to the server and add crontab backdoors.
The principle: every listening port that is not required is closed. Every required port is protected by authentication, encryption, and rate limiting. Firewall rules are default-deny — only explicitly listed ports are allowed.
Non-Standard Ports — Security Through Obscurity
Moving SSH from port 22 to port 2222 reduces automated brute-force noise (most scanners target well-known ports by default). A full port range scan (nmap -p 1-65535 -T4 host) finds the service in under 10 minutes. Non-standard ports reduce log noise but provide zero actual security. Real SSH hardening: key-based authentication only, fail2ban rate limiting, firewall source IP restrictions, and optionally a VPN requirement.
// CHAPTER 04
The Socket API — 40 Years of Stable Abstraction
socket(), bind(), listen(), accept(), connect() — how the Berkeley Sockets API works
// REAL-WORLD SCENARIO
In 1983, the Berkeley Software Distribution team added the socket API to 4.2BSD Unix. The design was elegant: a network connection is treated exactly like a file. You call socket() to get a file descriptor, read() and write() to transfer data, and close() when done. Applications have no idea whether the data travels across a room or across a continent — the OS handles all the complexity. This API, designed 40 years ago, runs on Linux, macOS, Windows (WinSock), iOS, Android, and every device connected to the internet today. A network program written in C for BSD in 1985 compiles and runs unchanged on modern Linux.
The core socket API calls:
Socket API — annotated with what each call does
# 1. socket() — create an endpoint
fd = socket(AF_INET, SOCK_STREAM, 0)
# AF_INET = IPv4, AF_INET6 = IPv6, AF_UNIX = local IPC
# SOCK_STREAM = TCP (reliable, ordered), SOCK_DGRAM = UDP (unreliable, fast)
# Returns: file descriptor (integer) — treat it like a file
# 2. bind() — associate socket with an address (server-side)
bind(fd, ("0.0.0.0", 8080))
# 0.0.0.0 = listen on all interfaces
# 127.0.0.1 = listen on localhost only (no external access)
# Clients usually skip bind() — OS auto-assigns an ephemeral source port
# 3. listen() — mark socket as passive (server-side)
listen(fd, 1024) # 1024 = accept queue depth
# Kernel now accepts TCP handshakes and queues completed connections
# Application calls accept() to retrieve them
# 4. accept() — retrieve a completed connection (server-side)
client_fd, client_addr = accept(fd)
# Returns a NEW socket fd for this specific connection
# Original fd stays listening for more connections
# New fd's 5-tuple: (server_ip:8080, client_ip:ephemeral_port)
# 5. connect() — establish a connection (client-side)
connect(fd, ("server.example.com", 443))
# Triggers TCP 3-way handshake
# Blocks until connected (or times out after ~127 seconds default)
# OS auto-assigns ephemeral source port
# 6. send/recv — transfer data
send(fd, data, 0) # Write to socket (kernel buffers it)
data = recv(fd, 4096, 0) # Read up to 4096 bytes (blocks if no data)
# recv() returns 0 when remote side closed connection (FIN received)
# 7. close() — initiate graceful shutdown
close(fd) # Sends FIN, begins 4-way teardown
TCP SOCKET CONNECTION LIFECYCLE
Step through the system calls from socket creation to teardown.
Socket Createdsocket()
CLIENT
socket(AF_INET, SOCK_STREAM, 0)
SERVER
socket(AF_INET, SOCK_STREAM, 0)
Both sides create a socket — an OS file descriptor representing a network endpoint. No network activity yet. The socket is not connected or bound. AF_INET = IPv4, SOCK_STREAM = TCP, 0 = default protocol.
Socket Options — Tuning Connection Behavior
Key socket options and when to use each
# SO_REUSEADDR — allow binding to port in TIME_WAIT
# Essential for server restart without waiting ~120 seconds
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# SO_REUSEPORT — allow multiple processes to bind the same address:port
# Kernel load-balances incoming connections across all bound processes
# Used by multi-worker servers (nginx worker processes all bind port 443)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
# TCP_NODELAY — disable Nagle algorithm (don't buffer small writes)
# Essential for interactive protocols: SSH, gaming, financial trading
# Without it, small writes are buffered for up to 200ms
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# SO_KEEPALIVE — send TCP keepalive probes on idle connections
# Detects dead peers (router rebooted, network cable pulled)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # First probe after 60s idle
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # Probe every 10s
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) # Close after 5 missed probes
# SO_RCVBUF / SO_SNDBUF — receive/send buffer size
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4194304) # 4 MB receive buffer
# Larger buffers = better throughput on high-latency paths (BDP tuning)
# SO_LINGER — control behavior when close() is called
# linger=True, timeout=0 → RST on close (no TIME_WAIT, immediate port reclaim)
# Useful for test servers; dangerous in production (may lose in-flight data)
import struct
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
// CHAPTER 05
The 5-Tuple and Connection Demultiplexing
How the kernel uses five fields to identify every active connection in O(1)
// REAL-WORLD SCENARIO
Your laptop connects to Netflix. Then you open a second Netflix window. Both connections go from 192.168.1.10 to Netflix's server (52.94.236.152) on port 443. How does the OS deliver the right video frames to the right window? The source port differs: window 1 might use port 54321, window 2 uses 54322. Both destination IP and port are identical — but the 5-tuples are different. The kernel maintains a hash table of connections indexed by 5-tuple. Incoming packets arrive, the 5-tuple is hashed, the bucket is found, and the packet lands in the right socket's receive buffer. Window 1 gets its frames, window 2 gets its frames, perfectly separated.
FIVE-TUPLE CONNECTION TABLE
Click any connection row to see why its 5-tuple is unique. Note connections 1 and 2 go to the same server.
Src IP
Src Port
Dst IP
Dst Port
Proto
State
Process
192.168.1.10
54321
142.250.80.46
443
TCP
ESTABLISHED
Chrome (google.com tab 1)
192.168.1.10
54322
142.250.80.46
443
TCP
ESTABLISHED
Chrome (google.com tab 2 — same server!)
192.168.1.10
54400
8.8.8.8
53
UDP
—
systemd-resolved (DNS query)
192.168.1.10
22100
10.0.0.5
22
TCP
ESTABLISHED
ssh (remote shell)
0.0.0.0
443
*
—
TCP
LISTEN
nginx (accepts all inbound HTTPS)
192.168.1.10
55000
142.250.80.46
443
UDP
—
Chrome (HTTP/3 QUIC — same dst, diff protocol)
192.168.1.10:54321 ↔ 142.250.80.46:443 (TCP)
This is the first Chrome tab connection. The kernel tracks this as a unique 5-tuple. Replies from 142.250.80.46:443 with dst port 54321 are delivered to this specific tab.
Kernel Connection Table: Implementation
The kernel maintains a hash table of active TCP connections, keyed by the full 5-tuple. When a TCP segment arrives:
Kernel TCP demultiplexing path (simplified)
1. Interrupt: NIC DMA's packet to ring buffer, signals CPU via interrupt
2. Kernel extracts IP header: src_ip, dst_ip, protocol
3. Kernel extracts TCP header: src_port, dst_port
4. Computes: hash(src_ip, src_port, dst_ip, dst_port, proto) → bucket
5. Walks bucket list to find matching socket
6. Copies segment data to socket's receive buffer (sk_buff)
7. Wakes any thread blocked in recv()/read() on that socket
If no match in ESTABLISHED table → check LISTEN sockets
If match in LISTEN → process new connection (send SYN-ACK, add to SYN queue)
If no match at all → send RST (connection refused)
This lookup is O(1) average case — critical when a server has millions of connections. Modern kernels use SipHash (a cryptographic PRF) seeded with a random secret at boot time to prevent hash collision attacks where an adversary crafts connections that all map to the same hash bucket, degrading lookup to O(n) and causing CPU exhaustion.
⚡ Modern Servers Handle 1–10 Million Connections Per Machine
Cloudflare, Google, and AWS load balancers handle 1–10 million simultaneous TCP connections per machine. The kernel's connection hash table, socket memory management, interrupt coalescing (NAPI batching — not one interrupt per packet but batching multiple packets per interrupt), and receive-side scaling (RSS — spreading interrupts across CPU cores) enable this scale. Linux with default settings handles ~65,000 connections before socket buffer memory exhausts; with tuning (rmem, wmem, somaxconn, file descriptor limits) it scales to millions.
// CHAPTER 06
Port Exhaustion — When Connections Run Out
The client-side scaling failure nobody talks about until it hits production
// REAL-WORLD SCENARIO
A microservice makes 50,000 outbound database connections per second, each with a 30ms average duration. Concurrent connections = 50,000 × 0.030s = 1,500 connections. Linux's default ephemeral range: 32,768–60,999 = 28,231 ports. That's fine. But after a database performance regression, queries slow to 800ms average. Now concurrent connections = 50,000 × 0.8 = 40,000. TIME_WAIT connections add another 30,000 (held 60 seconds each). Total: 70,000 connections to the same destination IP:port — exceeding the 28,231 ephemeral port limit. New connections fail with "Cannot assign requested address" (EADDRNOTAVAIL). The database appears "down" even though it's healthy. This is port exhaustion.
Port exhaustion occurs when a client runs out of available source ports to open new connections to a specific destination IP:port. The constraint: the 5-tuple must be unique. If all 28,231 ephemeral ports to a given (dst_ip:dst_port) are either active or in TIME_WAIT, the OS cannot open another connection.
TIME_WAIT: The Root Cause of Most Port Exhaustion
When a TCP connection closes (the active closer — the side that sends the first FIN), the connection enters TIME_WAIT for 2×MSL (Maximum Segment Lifetime = 30–60 seconds, so TIME_WAIT lasts 60–120 seconds). The OS keeps this state to: (1) ensure the final ACK reaches the remote peer, and (2) ensure any delayed packets from the old connection are discarded before a new connection reuses the 5-tuple.
On a client making 1,000 short-lived connections per second to the same server, TIME_WAIT accumulates: at 60s timeout, up to 60,000 TIME_WAIT entries exist simultaneously. With a 28K ephemeral port range, the client is 32K connections short of being able to open a new connection. This is the TIME_WAIT problem.
Diagnosing and fixing port exhaustion
# Diagnose port exhaustion
ss -s # Summary: connected, listening, TIME_WAIT counts
ss -tan | awk '{print $1}' | sort | uniq -c # Count by state
ss -tan state time-wait | wc -l # Count TIME_WAIT specifically
# Quick checks
cat /proc/sys/net/ipv4/ip_local_port_range # Ephemeral range
netstat -s | grep "SYNs to LISTEN|failed" # Connection failures
# SOLUTION 1: Connection pooling (best solution — root cause fix)
# Reuse existing connections instead of creating new ones per request
# PgBouncer for PostgreSQL, ProxySQL for MySQL
# pool_size = peak_QPS × avg_query_time_seconds
# Example: 10,000 QPS × 0.010s = 100 connections max needed
# SOLUTION 2: Expand ephemeral port range
sysctl -w net.ipv4.ip_local_port_range="1024 65535" # ~64K ports instead of ~28K
# SOLUTION 3: Enable tcp_tw_reuse (safe for clients)
sysctl -w net.ipv4.tcp_tw_reuse=1
# Allows reusing TIME_WAIT sockets for new outbound connections
# Requires TCP Timestamps (enabled by default on Linux)
# SAFE: client-side only, doesn't affect connection correctness
# SOLUTION 4: Multiple source IPs (each source IP gets its own port space)
ip addr add 10.0.0.2/24 dev eth0
# Now client can use 10.0.0.1 AND 10.0.0.2 as source IPs = 2× port space
# DANGER — do NOT use tcp_tw_recycle (removed in Linux 4.12)
# Breaks connections through NAT; was never safe for production
// CHAPTER 07
Server-Side: Accept Queue and Backlog
Why connections get refused under burst traffic and how to fix it
// REAL-WORLD SCENARIO
A web server is handling steady 10,000 requests/second without issue. A news article mentions the site; traffic spikes to 100,000 requests/second for 30 seconds. Users see "Connection refused." CPU: 20%. Memory: fine. The problem: the accept queue — where completed TCP handshakes wait for accept() to be called — is full at its default size of 128. The kernel is dropping incoming SYNs. Linux sends RST to every new connection attempt. Raising somaxconn to 65,536 and restarting nginx with a higher backlog fixes it — the burst is absorbed, connections queue, nginx processes them in order.
The Linux kernel maintains two queues for incoming connections on each listening socket:
SYN queue (incomplete connections): A SYN has arrived, a SYN-ACK was sent, waiting for the client's ACK to complete the 3-way handshake. Size controlled by net.ipv4.tcp_max_syn_backlog (default 128, production: 65536). Each entry uses ~200 bytes of kernel memory.
Accept queue (complete connections): 3-way handshake is complete. The connection is waiting for the application to call accept(). If the application is slow calling accept() (e.g., single-threaded, or under heavy CPU load), the queue fills. New completions are dropped, causing clients to see "Connection refused." Queue depth = min(backlog_arg_to_listen(), net.core.somaxconn).
Tuning the accept queue for high concurrency
# Production kernel tuning
sysctl -w net.core.somaxconn=65536 # Max accept queue depth
sysctl -w net.ipv4.tcp_max_syn_backlog=65536 # Max SYN queue depth
sysctl -w net.core.netdev_max_backlog=65536 # NIC receive queue depth
# Application: request high backlog in listen()
# Python
import socket
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 8080))
s.listen(65535) # Capped by somaxconn
# C
listen(fd, SOMAXCONN); // Use OS maximum
# nginx config (also needs OS tuning above)
# listen 443 ssl backlog=65535;
# Monitor queue state
ss -tlnp | grep ':443'
# Output: State Recv-Q Send-Q
# Recv-Q = current accept queue depth (> 0 means app is behind)
# Send-Q = max queue size
# Monitor drops
cat /proc/net/netstat | grep ListenOverflows
netstat -s | grep "listen queue" # Count dropped due to full queue
// CHAPTER 08
Non-Blocking I/O and Event-Driven Servers
From the C10K problem to epoll — why nginx handles 100K connections
// REAL-WORLD SCENARIO
1999: Dan Kegel publishes the "C10K Problem" paper asking how to handle 10,000 simultaneous network connections. At the time, Apache used a prefork model: one process per connection. 10,000 connections = 10,000 processes, each consuming 2–4 MB memory = 20–40 GB RAM just for process overhead. Impossible on 1990s hardware. The solution, already available in Linux: event-driven I/O with epoll. One thread monitors thousands of sockets; when any becomes readable/writable, it handles that socket. nginx and Node.js made this mainstream. Today a single nginx process routinely handles 100,000+ concurrent connections.
I/O multiplexing mechanisms (in order of scalability):
I/O multiplexing API evolution
select() — monitors up to FD_SETSIZE (1024) fds.
O(n) scan on EVERY call. Obsolete. Max 1024 connections.
poll() — removes 1024 limit. Still O(n) scan. Not scalable.
epoll() — Linux only (since 2.5.44, 2002). O(1) event notification.
(Linux) The kernel maintains a red-black tree of monitored fds.
When any fd becomes ready, kernel adds it to a ready list.
epoll_wait() returns ONLY ready fds — no scanning.
Supports edge-triggered (ET) and level-triggered (LT) modes.
Used by: nginx, Redis, Node.js (libuv), PostgreSQL.
kqueue() — BSD/macOS equivalent of epoll. Same O(1) semantics.
(BSD/macOS)
io_uring — Linux 5.1+ (2019). Async I/O that avoids syscall overhead.
Batches I/O operations through shared ring buffers.
Application submits operations without syscall (mmap'd ring).
Used by: Tokio (Rust), io_uring-aware web servers.
epoll server pattern (Python asyncio)
import asyncio
async def handle_client(reader, writer):
while True:
data = await reader.read(4096)
if not data:
break
writer.write(b"Echo: " + data)
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle_client, '0.0.0.0', 8888, backlog=65535)
async with server:
await server.serve_forever()
asyncio.run(main())
# asyncio uses epoll (Linux), kqueue (macOS), or IOCP (Windows) automatically
# Single event loop handles 100,000 concurrent connections in one thread
The key insight of event-driven servers: blocking on network I/O wastes CPU. A thread blocked in recv() waiting for a client to send data is doing nothing — but it consumes a thread stack (8 MB default on Linux). With 10,000 blocked threads that's 80 GB of stack memory just for waiting. Non-blocking sockets + epoll let one thread service thousands of connections — it only runs when there's actual data to process.
// CHAPTER 09
Unix Domain Sockets — Local IPC via the Socket API
Same API, no network stack — why your database connection is 30% faster using /tmp/postgres.sock
// REAL-WORLD SCENARIO
A web application and its PostgreSQL database run on the same machine. Option A: connect via TCP — 127.0.0.1:5432. This works, but involves the full TCP stack: socket creation, 3-way handshake, ACK for every segment, sequence numbers, checksums. Option B: connect via Unix domain socket — /var/run/postgresql/.s.PGSQL.5432. No TCP overhead, no IP stack, no network interface. Data transfers through a kernel memory buffer (a memcpy). Latency drops from ~50μs to ~20μs. CPU usage drops. No handshake means no connection setup overhead. Every major database uses Unix sockets for local connections.
Unix domain sockets (AF_UNIX, also called AF_LOCAL) use the exact same BSD socket API as TCP sockets — socket(), bind(), listen(), accept(), connect(), read(), write(), close() — but communicate entirely within the kernel's virtual file system, without any network stack involvement.
Instead of binding to an IP:port, a Unix socket binds to a filesystem path (e.g., /tmp/myapp.sock). The socket appears as a special file in the filesystem. Access control is enforced by Unix file permissions on the socket file — only users with read+write permission on the file can connect. This gives Unix sockets stronger access control than TCP (where any process can connect to 127.0.0.1:5432 if it has network access).
Unix domain socket usage in production systems
# Common Unix domain socket paths
/var/run/postgresql/.s.PGSQL.5432 # PostgreSQL
/var/lib/mysql/mysql.sock # MySQL/MariaDB
/var/run/redis/redis.sock # Redis (when configured)
/run/nginx.sock # nginx upstream to PHP-FPM
/var/run/docker.sock # Docker daemon API
/run/systemd/private/io.systemd.PrivateUsers # systemd internal
# Connect to PostgreSQL via Unix socket
psql -h /var/run/postgresql -U username dbname
# Or simply: psql -U username dbname (auto-finds socket)
# Connect to MySQL via Unix socket
mysql -S /var/lib/mysql/mysql.sock -u root
# Python: explicitly use Unix socket
import socket
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect('/tmp/myapp.sock')
# Python: PostgreSQL via Unix socket (psycopg2)
import psycopg2
conn = psycopg2.connect(host='/var/run/postgresql', dbname='mydb', user='user')
# Equivalent to TCP: host='127.0.0.1', port=5432
⚠ The Docker socket is root-equivalent
/var/run/docker.sock is the Docker daemon's control socket. Any process with write access to it can make Docker API calls — including spawning a new container with --privileged --volume /:/host and accessing the entire host filesystem as root. Mounting the Docker socket into a container (a common pattern for CI systems and monitoring tools) is a complete privilege escalation path from container to host. Only mount it when absolutely required, in containers with restricted network access and monitored execution.
// CHAPTER 10
TCP Socket States
The full state machine — from CLOSED to ESTABLISHED to TIME_WAIT
TCP connections move through a defined set of states. Understanding these states is essential for diagnosing connection failures, interpreting ss / netstat output, and tuning server performance.
TCP state machine — key states and transitions
CLOSED → Initial state. No connection.
LISTEN → Server has called listen(). Waiting for incoming SYNs.
(Server side only. Shows in ss -tlnp output.)
SYN_SENT → Client called connect(), sent SYN. Waiting for SYN-ACK.
(If stuck here: server unreachable or firewall dropped SYN)
SYN_RCVD → Server received SYN, sent SYN-ACK. Waiting for final ACK.
(SYN flood attack fills this state for many spoofed IPs)
ESTABLISHED → 3-way handshake complete. Data transfer phase.
(Normal connected state. Both sides.)
FIN_WAIT_1 → Active closer sent FIN. Waiting for ACK.
FIN_WAIT_2 → ACK received for our FIN. Waiting for remote FIN.
(net.ipv4.tcp_fin_timeout controls max time here — default 60s)
CLOSE_WAIT → Remote sent FIN; we sent ACK. App must call close() to continue.
⚠ Many CLOSE_WAIT entries = application not calling close() promptly
(Often a resource leak bug)
LAST_ACK → Passive closer sent FIN after receiving FIN. Waiting for final ACK.
TIME_WAIT → Active closer received remote FIN, sent final ACK. Waiting 2×MSL.
⚠ Many TIME_WAIT entries = many short-lived connections from one client
(Normal but can cause port exhaustion. See Chapter 6.)
CLOSED → Connection fully terminated.
Diagnostic tip: ss -tan | awk '{print $1}' | sort | uniq -c shows counts by state. A healthy server predominantly shows ESTABLISHED. A server with many CLOSE_WAIT entries has an application-level resource leak (not calling close()). A client with many TIME_WAIT entries is making many short-lived connections to the same destination — consider connection pooling.
// CHAPTER 11
Port Scanning and Service Discovery
How nmap works, what attackers see, and interpreting scan results
// REAL-WORLD SCENARIO
A network security engineer needs to verify that a newly deployed server only exposes intended services. She runs nmap -sS -p 1-65535 server_ip from an external perspective, exactly as an attacker would. The scan completes in 4 minutes and shows: 22/tcp open (SSH), 443/tcp open (HTTPS), 3306/tcp open (MySQL — not intended!). The MySQL service was left listening on all interfaces because a developer set bind-address=0.0.0.0 for "easier local testing." An internet-exposed MySQL with default credentials is a critical finding. The engineer escalates immediately.
How TCP Port Scanners Work
Port scanners exploit the kernel's predictable behavior at the socket level:
SYN scan (half-open, -sS): Send a SYN packet. If port is open: receive SYN-ACK (immediately send RST, never completing the handshake). If port is closed: receive RST. If firewalled: no response (timeout). Faster than full connect scan; may not appear in application logs since the handshake never completes. Requires raw socket access (root).
Connect scan (-sT): Complete the full 3-way handshake (use connect() syscall). No root required. Leaves entries in application logs. Useful when running without root privileges.
UDP scan (-sU): Send a UDP packet. If port closed: receive ICMP Port Unreachable (type 3, code 3). If open: either no response or a service-specific response. Slow due to ICMP rate limiting (Linux limits ICMP unreachables to 1/second by default).
nmap scanning reference
# SYN scan — fastest, requires root
nmap -sS -p 1-1024 target # Top 1024 ports
nmap -sS -p 1-65535 target # Full scan
nmap -sS --top-ports 100 target # nmap's 100 most common ports
# Connect scan — no root required
nmap -sT target
# UDP scan of common services
nmap -sU --top-ports 20 target # DNS(53), SNMP(161), NTP(123)...
# Service and version detection
nmap -sV target # Identify service versions (banner grab)
nmap -A target # SYN + version + OS + scripts + traceroute
# Specific port checks
nmap -p 22,80,443,3306 target
# Subnet scan
nmap -sS 192.168.1.0/24 # All hosts in /24
# Output formats
nmap -sS -oN output.txt target # Normal output
nmap -sS -oX output.xml target # XML for parsing
nmap -sS -oG output.gnmap target # Grepable format
# Quick ping sweep (host discovery only)
nmap -sn 10.0.0.0/24
// CHAPTER 12
Firewall Rules and the 5-Tuple
Translating security policy to packet-level rules using port knowledge
Firewall rules are pattern-matched against the 5-tuple of each packet. Understanding port numbers and socket states directly translates to writing correct firewall rules. The most important concept: stateful inspection — tracking connection state so return traffic for established connections is automatically allowed without explicit rules for the return direction.
iptables rules — server security policy
# Default: deny all inbound, allow all outbound
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback (localhost communication)
iptables -A INPUT -i lo -j ACCEPT
# Allow established/related (return traffic for outbound connections)
# This allows: responses to our outbound requests, ICMP errors, FTP data
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# SSH with rate limiting (prevent brute force)
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m limit --limit 5/min --limit-burst 10 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP # Drop excess
# HTTPS
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# HTTP (redirect to HTTPS — still needs to be open)
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
# ICMP — allow for ping and PMTUD (fragmentation needed)
iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 10/sec -j ACCEPT
# Log dropped packets (for incident investigation)
iptables -A INPUT -j LOG --log-prefix "IPTABLES DROP: " --log-level 4
iptables -A INPUT -j DROP
Connection Refused vs. Connection Timeout
The single most important diagnostic distinction in connection troubleshooting:
Connection refused: The remote OS received the SYN and sent RST back. The machine is reachable. Either no service is listening on that port, or the application called close() sending RST. Fix: check that the service is running (ss -tlnp | grep PORT).
Connection timeout: The SYN was sent, no response received after ~127 seconds (Linux TCP retransmit timeout). Either the destination is unreachable (routing problem), or a firewall is silently dropping packets (DROP rule, not REJECT rule). A REJECT rule sends ICMP unreachable back (fast failure); a DROP rule causes the slow timeout. Fix: check routing, check intermediate firewall rules.
// CHAPTER 13
Troubleshooting Connection Issues
Systematic diagnosis from socket layer to application layer
Connection diagnostic toolkit
# ── LAYER 3: Is the host reachable? ─────────────────────────────────
ping -c 4 10.0.0.1 # ICMP echo (blocked by some firewalls)
traceroute 10.0.0.1 # Hop-by-hop path trace
# ── LAYER 4: Is the port open? ───────────────────────────────────────
nc -zv 10.0.0.1 443 # TCP connect test
# "succeeded" = port open
# "refused" = port not open or actively rejected (RST)
# (timeout) = firewall DROP or host unreachable
nc -u -zv 10.0.0.1 53 # UDP port test
# ── LOCAL: Is the service listening? ────────────────────────────────
ss -tlnp | grep ':443' # TCP listening on 443?
ss -ulnp | grep ':53' # UDP listening on 53?
ss -tanp | grep 'ESTABLISHED' # All active connections
# ── FIREWALL: Is traffic being blocked? ─────────────────────────────
iptables -L -n -v | grep 443 # Check rules for port 443
iptables -L -n --line-numbers # All rules with line numbers
# ── PACKET CAPTURE: What's actually happening? ──────────────────────
tcpdump -i eth0 -n 'host 10.0.0.1 and port 443' -c 20
# If you see: SYN → SYN-ACK → ACK = connected
# If you see: SYN → SYN → SYN = server not responding (firewall drop)
# If you see: SYN → RST = refused (no listener or app rejected)
# ── APPLICATION: TLS/SSL check ───────────────────────────────────────
openssl s_client -connect 10.0.0.1:443 -servername hostname
# Shows certificate, cipher suite, TLS version
# ── SYSCALL TRACE: What's the app doing? ────────────────────────────
strace -e trace=network -p PID # All network syscalls for a process
// CHAPTER 14
Common Misconceptions
Port and socket errors that waste hours in production debugging
✗ Common Mistake — A server can only handle 65,535 simultaneous connections
Port numbers are 16-bit, giving 65,535 possible values — but this limit applies to the source port used by one client IP when connecting to one server IP:port. A server handling connections from thousands of different client IPs has thousands of different 5-tuples per port. There is no 65,535 limit on server-side connections. A server with 1 million clients connecting to port 443 has 1 million unique 5-tuples, all valid simultaneously. The per-server limit is memory (each connection uses ~4KB kernel memory) and file descriptor limits (ulimit -n, default 1024 — must be raised to 1M+ for high-performance servers).
✗ Common Mistake — Opening a connection 'uses up' a port on the server
The server does not allocate a new port for each incoming connection. Every connection uses the same server port (e.g., 443). The accept() call returns a new socket file descriptor — not a new port. The kernel differentiates connections by the full 5-tuple, not just the server port. Port exhaustion is exclusively a client-side problem: clients run out of ephemeral source ports when connecting to the same destination IP:port.
✗ Common Mistake — Connection refused and connection timeout mean the same thing
They indicate completely different problems. Connection refused = RST received = the destination machine is reachable and actively rejecting the connection (no listener on that port, or explicit reject rule). The failure is fast (immediate RST). Connection timeout = no response = either the destination is unreachable, or a firewall is silently dropping packets (DROP vs REJECT). The failure is slow (127 seconds default). These require entirely different remediation: refused → fix the service; timeout → fix routing or firewall.
✗ Common Mistake — SO_REUSEADDR and SO_REUSEPORT do the same thing
They solve different problems. SO_REUSEADDR allows a new server process to bind a port that has lingering TIME_WAIT sockets from the previous server instance — essential for restarting a server without a 120-second wait. SO_REUSEPORT allows multiple simultaneously-running processes to bind the exact same address:port — the kernel distributes incoming connections across all of them. This enables a multi-process server model where nginx spawns 8 worker processes, each calling listen() on port 443 independently.
✗ Common Mistake — Closing a connection immediately frees the port
After close(), a TCP connection in TIME_WAIT still occupies a kernel connection table entry for 2×MSL (60–120 seconds). The 5-tuple cannot be reused for a new connection to the same destination during this period. This is by design — to prevent a new connection from receiving delayed packets from the old connection. High-throughput clients making thousands of short-lived connections to the same server accumulate TIME_WAIT entries that can exceed the ephemeral port range. Fix: connection pooling (avoid creating/destroying connections), tcp_tw_reuse, or multiple source IPs.
✗ Common Mistake — Moving a service to a non-standard port makes it secure
Moving SSH from port 22 to port 2222 reduces automated brute-force noise — most scanners specifically target well-known ports. But a full port scan (nmap -p 1-65535 target) finds any service in under 10 minutes. Security through obscurity is not a security control. Actual SSH security: key-based authentication (no passwords), fail2ban rate limiting, firewall source IP whitelisting, and regular patching. Non-standard ports are a noise filter, not a barrier.
// CHAPTER 15
Interview Questions
Beginner
What is the difference between a port and a socket?
A port is a 16-bit number (0–65,535) identifying a service or process endpoint on a machine. Port 443 means HTTPS; port 22 means SSH. A socket is an OS file descriptor representing one endpoint of a network connection. Multiple sockets can use the same port (every client connected to a web server has their own socket, all sharing the server's port 443). A socket address is an (IP, port) pair. A TCP connection is identified by two socket addresses plus the protocol — the 5-tuple. When you call socket() in code, you get a file descriptor; that fd is the socket.
Beginner
Explain the 5-tuple. Why must it be unique, and what happens when two connections would share the same 5-tuple?
The 5-tuple is (source_ip, source_port, destination_ip, destination_port, protocol). It is the kernel's demultiplexing key for incoming packets — every packet is matched against the connection table using this key to find the right socket. If two active connections had identical 5-tuples, the kernel could not determine which connection a packet belongs to. The OS enforces uniqueness: connect() fails with EADDRINUSE if the resulting 5-tuple is already in use. Connections in TIME_WAIT also prevent 5-tuple reuse until they expire. The OS automatically picks a different ephemeral source port if the requested one would create a duplicate.
Intermediate
What causes port exhaustion, and how would you diagnose and fix it?
Port exhaustion occurs when a client runs out of ephemeral source ports when connecting to the same destination IP:port. The 5-tuple must be unique — if all ~28K ephemeral ports (Linux default range 32768–60999) to a given destination are either active or in TIME_WAIT (held 60–120s after close), no new connections can be opened (EADDRNOTAVAIL error). Diagnosis: ss -tan | awk '{print $1}' | sort | uniq -c shows TIME_WAIT counts; ss -s shows socket summary. Fixes: (1) Connection pooling — reuse connections instead of creating new ones per request (root cause fix). (2) Expand ephemeral range: sysctl -w net.ipv4.ip_local_port_range="1024 65535". (3) Enable tcp_tw_reuse=1 — allows safe reuse of TIME_WAIT sockets for new outbound connections. (4) Multiple source IPs — each source IP has its own 64K port space.
Senior
Why does epoll achieve O(1) complexity compared to select's O(n), and what does this mean for server scalability?
select() requires the application to pass the complete set of file descriptors to monitor on every call. The kernel scans all of them to find which are ready — O(n) per call. With 100,000 connections, the kernel scans 100,000 fds even if only 1 is ready. epoll() uses a kernel-maintained data structure (red-black tree for the interest set, linked list for ready events). When any fd becomes ready, the kernel adds it to the ready list. epoll_wait() returns only fds that are actually ready — the application processes exactly those, no scanning. This is O(1) in the number of ready events regardless of total monitored fds. Scalability impact: nginx with select() would spend most CPU time scanning idle connections. With epoll(), nginx only processes connections with actual data. This is why nginx handles 100,000 simultaneous connections while Apache prefork (one process per connection, select() based) could not scale past a few thousand on typical hardware.
Senior
Explain the TCP TIME_WAIT state — why it exists, why it matters, and when it's safe to bypass it.
TIME_WAIT is the state a TCP connection's active closer enters after completing the 4-way FIN exchange. It lasts 2×MSL (Maximum Segment Lifetime = 30–60s, so TIME_WAIT = 60–120s). It exists for two reasons: (1) Reliable close: ensure the final ACK (for the remote's FIN) is delivered. If the final ACK is lost, the remote retransmits its FIN. TIME_WAIT allows the ACK to be retransmitted. (2) Prevent 5-tuple reuse: ensure any delayed packets from the old connection are discarded before a new connection can reuse the same 5-tuple. Without TIME_WAIT, a delayed packet from a previous connection could corrupt a new connection. TIME_WAIT matters for high-throughput clients: at 10,000 connections/second to one server, TIME_WAIT accumulates 600,000–1,200,000 entries simultaneously — exceeding the ephemeral port range. Safe bypasses: tcp_tw_reuse (allows client-side reuse when TCP Timestamps confirm the old connection is truly gone — safe). tcp_tw_recycle (was unsafe for NATted networks; removed in Linux 4.12). SO_LINGER with timeout=0 (sends RST instead of FIN — avoids TIME_WAIT but may lose in-flight data; acceptable for test environments, dangerous in production).
PhD
Describe the hash collision attack against TCP connection tables and how Linux mitigated it.
The Linux kernel's TCP connection table is a hash table keyed by 5-tuple. Lookup is O(1) average when connections distribute uniformly across buckets. If an attacker can predict the hash function and craft 5-tuples that all hash to the same bucket, the hash table degrades to a linked list — O(n) lookup per packet, causing CPU exhaustion proportional to connections-per-bucket. This was a real attack: early Linux used Bob Jenkins' hash function seeded with a deterministic value (or values predictable from network timing). An attacker could compute which source IPs and ports would collide. Mitigation: Linux (and other modern kernels) switched to SipHash — a cryptographic pseudo-random function (PRF) seeded with a secret random value generated at boot time. An attacker who cannot predict the hash secret cannot craft colliding 5-tuples. Even if they try exhaustive probing, the hash function's output is computationally unpredictable from observable inputs. Modern Linux also adds additional randomization per-network-namespace. This is an example of algorithm security — the correct algorithm provides O(1) amortized complexity even in adversarial conditions.
🎯 Key Takeaways
✓Ports (0–65,535) identify services on a machine. The 5-tuple (src_ip, src_port, dst_ip, dst_port, protocol) uniquely identifies every active connection — the kernel's demultiplexing key.
✓Port ranges: 0–1023 well-known (root required to bind), 1024–49151 registered, 49152–65535 ephemeral (OS-assigned for client-side source ports, Linux default 32768–60999).
✓A server handles unlimited connections on one port — the server's port is constant; client diversity (src_ip:src_port) makes each 5-tuple unique. Port exhaustion is client-side only.
✓Socket API: socket()→bind()→listen()→accept() for servers; socket()→connect() for clients. File descriptors — treat them like files. Same API for TCP, UDP, and Unix domain sockets.
✓Port exhaustion: client runs out of ephemeral ports to a specific dst_ip:port. Fix with connection pooling (best), expanded ephemeral range, tcp_tw_reuse, or multiple source IPs.
✓TIME_WAIT lasts 60–120s after connection close — ensures safe 5-tuple reuse. tcp_tw_reuse safely allows client-side reuse; never use tcp_tw_recycle (removed in Linux 4.12).
✓Accept queue: completed handshakes waiting for accept() — raise net.core.somaxconn to 65536 for production. Full queue = connections refused under burst traffic.
✓epoll (Linux) achieves O(1) event notification regardless of connection count. select is O(n). nginx/Node.js use epoll for 100K+ concurrent connections per process.
✓Unix domain sockets use the same API as TCP but bypass the network stack — ~30% faster for local IPC. Used by PostgreSQL, MySQL, Redis, and nginx-to-PHP-FPM communication.
✓Connection refused (RST received) ≠ connection timeout (no response). Refused = port not open or actively rejected. Timeout = firewall DROP rule or host unreachable. Different problems, different fixes.
Share
Discussion
0
Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.