OS Networking Stack
Trace Linux TCP/IP traffic through socket buffers (sk_buff), protocol layers, kernel queues, and routing, and use tools to diagnose network failures.
Linux’s networking stack carries packets from the NIC through link, IP, and transport layers to application sockets. This guide explains `sk_buff`, TCP state and flow control, routing, netfilter, and the kernel queues that shape connection handling. It also shows how to inspect sockets and packets, monitor drops and retransmissions, and diagnose buffer or connection exhaustion. Use the layer-by-layer model to narrow down where a packet stalls before changing kernel settings.
OS Networking Stack
Introduction
When you type a URL into your browser, something remarkable happens beneath the surface. That HTTP request does not simply appear on the wire. It cascades through layers of abstraction, each handling a specific concern, before finally becoming a stream of electrons or photons traveling through physical infrastructure. Understanding this journey reveals why modern operating systems can handle millions of concurrent connections without breaking a sweat.
The Linux networking stack is a masterpiece of engineering. It handles everything from basic Ethernet frames to complex TCP congestion control algorithms, all while maintaining the performance characteristics that servers demand. Whether you are debugging latency issues, optimizing throughput, or simply trying to understand what happens when your service calls an external API, the networking stack is essential knowledge.
This post peels back the layers from physical wire to application socket, examining the data structures and algorithms that make network communication possible.
When to Use / When Not to Use
Understanding the networking stack becomes critical when you need to optimize network performance, debug connectivity issues, or implement network-related kernel features. Engineers working on high-performance proxies, load balancers, or distributed systems benefit most from deep networking stack knowledge.
The stack shines when you need fine-grained control over connection behavior, want to implement custom protocols, or must debug issues that span multiple layers. Knowledge of the stack helps you interpret ss, netstat, and tcpdump output meaningfully, turning cryptic hex dumps into actionable insights.
However, most application developers never need to work directly with the kernel networking implementation. If you are building web applications or REST APIs, the socket abstraction provided by your language runtime is sufficient. The stack becomes relevant only when you encounter unexplained latency, need to handle connection counts in the hundreds of thousands, or are debugging tricky firewall or routing issues.
Architecture or Flow Diagram
graph TD
A[Application Layer<br/>HTTP, DNS, etc.] --> B[Socket API<br/>send/recv]
B --> C[TCP/UDP Layer<br/>Connection handling]
C --> D[IP Layer<br/>Routing, Forwarding]
D --> E[Netfilter/Iptables<br/>Packet filtering]
E --> F[Link Layer<br/>Ethernet, ARP]
F --> G[Network Driver<br/>NIC Interface]
G --> H[Physical Network<br/>Copper, Fiber]
H --> G
G --> F
F --> E
E --> D
D --> C
C --> B
B --> A
subgraph Kernel Space
C
D
E
F
end
subgraph Userspace
A
B
end
Incoming packets are typically DMA-transferred by the NIC into host memory. The driver associates that data with an sk_buff and passes it up through the protocol layers; exact buffer handling varies by driver and offload features. Each layer examines or updates packet metadata until the payload reaches the target socket, where the application can read it.
Outgoing packets flow in the opposite direction. The application writes data to a socket, which places it in a socket send buffer. The TCP layer encapsulates data in TCP segments with proper sequence numbers and flags, then passes these to the IP layer for routing. The IP layer adds routing information and passes to the link layer, which adds Ethernet headers and hands the packet to the driver for transmission.
Core Concepts
Socket Buffers (sk_buff)
The sk_buff structure is the fundamental data unit in the Linux networking stack. Every packet that travels through the stack exists as an sk_buff at some point. Understanding its structure illuminates how the stack achieves zero-copy operations and efficient header traversal.
struct sk_buff {
struct sk_buff *next; // Next buffer in chain
struct sk_buff *prev; // Previous buffer in chain
struct sock *sk; // Owner socket
unsigned int len; // Length of data
unsigned int data_len; // Length of fragment data
__u16 protocol; // Protocol identifier
__u8 transport_header; // Offset to transport header
__u8 network_header; // Offset to network header
__u8 mac_header; // Offset to link layer header
char *head; // Start of buffer allocation
char *data; // Start of data
char *tail; // End of data
char *end; // End of buffer allocation
};
The clever part of sk_buff design is the headroom and tailroom concept. When an incoming packet arrives, the driver allocates a buffer with space before and after the actual packet data. This allows headers to be added or stripped without reallocating memory or copying data. The push and pull operations simply adjust pointers.
TCP Implementation Details
TCP in Linux implements congestion control through pluggable algorithms. The kernel provides an interface for different congestion control algorithms that can be selected per connection or system-wide. Standard algorithms include CUBIC (default in many distributions), BBR, and Reno.
# List available congestion control algorithms
sysctl net.ipv4.tcp_available_congestion_control
# Set algorithm for new connections
sysctl -w net.ipv4.tcp_congestion_control=bbr
# View current algorithm in use
sysctl net.ipv4.tcp_congestion_control
TCP connections progress through distinct state machine states: LISTEN, SYN_SENT, SYN_RECEIVED, ESTABLISHED, FIN_WAIT, CLOSE_WAIT, CLOSING, and TIME_WAIT. Each state has specific timeout values and transition rules. Understanding these states helps diagnose connection issues like sockets stuck in CLOSE_WAIT because the application stopped reading.
Protocol Layers
The networking stack implements a layered architecture where each layer has specific responsibilities. The application layer handles protocol-specific logic like HTTP parsing. TCP provides reliable, ordered delivery with congestion control. IP handles routing between networks. Ethernet provides local network delivery through MAC addresses.
Each layer communicates with adjacent layers through standard interfaces. This modularity allows the same TCP implementation to work over Ethernet, WiFi, or any other link layer. It also allows new protocols to be implemented without modifying lower layers.
Each layer owns a specific header structure that travels with the packet. The TCP header sits between the application data and the IP header, holding source and destination ports, sequence numbers, acknowledgment numbers, flags (SYN, ACK, FIN, RST), and a window size for flow control. The IP header contains source and destination IP addresses, TTL, and a protocol number that tells the next layer whether TCP, UDP, or ICMP follows. The Ethernet header carries destination and source MAC addresses plus an EtherType field identifying the protocol above it (0x0800 for IPv4, 0x86DD for IPv6).
When a packet arrives at a host, each layer decapsulates by stripping its own header and passing the payload upward. The Ethernet driver checks the EtherType, removes the Ethernet header, and hands the payload to IP. IP consults the routing table, strips its header, and passes data to TCP or UDP. TCP validates sequence numbers, sends acknowledgments, and pushes the application data up to the socket layer. A corrupted header at any layer breaks everything above it.
Layer boundaries also define where firewall rules hook in. The iptables INPUT chain runs after IP decapsulation but before TCP processes the segment. The OUTPUT chain applies to locally generated packets before they reach IP for encapsulation. Knowing which layer owns which header makes it easier to trace where a packet gets modified, dropped, or NATed. DNAT, for instance, works at the IP layer, rewriting the destination address before routing decisions are made. Connection tracking sits above TCP, maintaining state across the full TCP handshake and teardown.
Production Failure Scenarios
Scenario: TCP Connection Exhaustion
Problem: Server cannot accept new connections despite available resources. The listening socket’s accept queue fills faster than applications can process connections.
Symptoms: Connections timeout, ss -s shows high connection count, netstat -an | grep TIME_WAIT shows many connections stuck in TIME_WAIT.
Mitigation: Check whether the bottleneck is the accept queue, ephemeral ports, or file descriptors before changing kernel settings. Tune the listen() backlog and somaxconn for accept pressure, and consider connection pooling to reduce churn. tcp_tw_reuse behavior depends on kernel version and workload, so check the current kernel documentation before changing it. Do not use tcp_tw_recycle; Linux removed that setting in version 4.12.
# Increase local port range for outgoing connections
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# Enable TIME_WAIT reuse
sysctl -w net.ipv4.tcp_tw_reuse=1
# Increase TIME_WAIT bucket count
sysctl -w net.ipv4.tcp_max_tw_buckets=2000000
Scenario: NIC Interrupt Coalescence Issues
Problem: High latency variance or low throughput caused by suboptimal interrupt handling.
Symptoms: Latency spikes in latency-sensitive applications, lower than expected throughput despite CPU utilization being low. cat /proc/interrupts shows uneven distribution across CPUs.
Mitigation: Tune NIC interrupt moderation settings. Modern NICs support interrupt coalescing that batches interrupts to reduce CPU overhead, at the cost of increased latency. Find the balance based on workload characteristics.
# Check current interrupt settings
ethtool -c eth0
# Set optimal coalescing for low latency
ethtool -C eth0 rx-frames 1 tx-frames 1
Scenario: Socket Buffer Exhaustion
Problem: Socket receive or send buffers fill up, causing blocking or dropped data.
Symptoms: Applications block on writes, ss -m shows high socket buffer usage, logs show “Connection reset by peer” errors.
Mitigation: Increase socket buffer limits. The kernel enforces per-socket and system-wide limits on buffer sizes.
# View current limits
sysctl -a | grep tcp_rmem
sysctl -a | grep tcp_wmem
# Increase buffer sizes (min, default, max)
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
Trade-off Table
| Aspect | TCP | UDP |
|---|---|---|
| Reliability | Guaranteed ordered delivery with retransmission | No delivery guarantees |
| Latency | Higher due to flow control and acknowledgments | Lower with no connection overhead |
| Throughput | Congestion control supports bulk transfers | Avoids TCP overhead; app and network limits still apply |
| Connection Overhead | 3-way handshake required before data transfer | No connection setup required |
| Resource Usage | Higher per-connection memory for state tracking | Minimal connection state |
| Flow Control | Receiver advertises a window to limit in-flight data | No built-in flow control |
| Aspect | Polling | Interrupt-Driven |
|---|---|---|
| Latency | Lower latency for high-rate traffic | Higher latency per packet |
| CPU Overhead | Constant CPU usage regardless of traffic | CPU usage proportional to traffic |
| Power Efficiency | Wastes CPU cycles when idle | Better power efficiency |
| Scalability | Poor scaling to many interfaces | Scales well with many connections |
| Aspect | Kernel Processing | DPDK Userspace |
|---|---|---|
| Latency | Higher (kernel-user context switches) | Lower (avoids context switches) |
| Throughput | Moderate (kernel overhead) | Very high (batch processing) |
| Development | Standard socket API | Requires specialized code |
| Security | Kernel provides isolation | More attack surface |
| Portability | Works across all hardware | Hardware-specific drivers |
Implementation Snippets
Creating a TCP Server Socket
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int create_tcp_server(int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) return -1;
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = INADDR_ANY,
.sin_port = htons(port)
};
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(sockfd);
return -1;
}
listen(sockfd, 128); // Backlog of 128 pending connections
return sockfd;
}
Inspecting Socket Statistics with ss
# Show all TCP sockets with process info
ss -tulnp
# Show socket memory usage
ss -m
# Show TCP timers
ss -ti
# Show detailed connection info for specific IP
ss -dst src 192.168.1.100
Capturing Packets with tcpdump
The tcpdump command intercepts packets as they flow through the networking stack and prints headers and payload data to stdout. Unlike ss which queries kernel state, tcpdump sees actual packets in flight. The -i flag picks the interface, -w dumps raw packets to a file, and -r reads a file back. Without -w, output goes straight to your terminal.
# Capture HTTP traffic on port 80
tcpdump -i eth0 -w capture.pcap 'tcp port 80'
# View capture with ASCII content
tcpdump -r capture.pcap -A
# Capture packets to/from specific host
tcpdump -i eth0 host 192.168.1.100 and tcp
# Show packet headers without full capture
tcpdump -i eth0 -nn -c 10
The -nn flag turns off DNS resolution and service name translation, so you get raw IP addresses and port numbers. This matters because DNS lookups during capture add latency and can fail when the network is already struggling. The -A flag dumps ASCII payload, so you can read HTTP headers or DNS queries directly. Encrypted traffic shows up as raw bytes and TCP headers only.
The -w capture.pcap option writes pcap format, which Wireshark reads with protocol decoding. Wireshark can reassemble TCP streams and decode protocols present in the capture. This pairing is useful for investigating network behavior.
Under heavy load, the capture process can fall behind and drop packets. Use -B 4096 to give it a larger buffer. Use -s 0 to request full packets; the default snap length varies by platform and libpcap version.
Observability Checklist
Network observability splits into three areas: kernel parameters that set hard limits, runtime metrics that show what the stack is actually doing, and tools for digging into specific problems.
Kernel Parameters to Monitor:
These sysctls set the ceiling on stack behavior. Hit one of these limits and you get drops or connection failures even when memory and CPU are fine.
net.ipv4.tcp_tw_reuse- Enable TIME_WAIT reusenet.core.netdev_max_backlog- Per-interface packet queue depthnet.ipv4.tcp_max_syn_backlog- SYN queue lengthnet.ipv4.ip_local_port_range- Ephemeral port range
Metrics to Track:
Sample these regularly with a monitoring agent.
- Socket count per state (
ss -s) — breaks down connections by state; TIME_WAIT or CLOSE_WAIT spikes mean the app is not closing connections properly - Interface errors and drops (
ip -s link) — errors point to cable or duplex issues; drops mean a queue hit its limit - TCP retransmissions (
netstat -s | grep retransmitted) — above 1% signals congestion or corruption somewhere in the path - Connection attempt failures — SYN arrivals without matching accepts signal accept queue overflow
- Socket buffer utilization (
ss -m) — actual memory per socket; compare againsttcp_rmemandtcp_wmemto see if you are near the ceiling
Tools for Investigation:
Metrics tell you something is wrong. These tools tell you what.
ss- Socket statistics, faster than netstat, shows connection states, timers, and memory usageip- Interface and routing info, also exposes link-level stats and address assignmentstcpdump- Packet capture, see the tcpdump section abovewireshark- GUI packet analyzer, reads pcap files and applies protocol dissectors to reconstruct streamsethtool- NIC hardware settings: interrupt coalescing, ring buffer sizes, offload features
Network Hardening and Compliance
Network stack security requires defense in depth across multiple layers.
Firewall Configuration: Use iptables or nftables to implement network segmentation and access controls. Default-deny policies reduce attack surface. Each service should communicate only with required endpoints.
TCP Stack Hardening: Disable unused TCP features to reduce attack surface. Disable tcp_sack if you do not need selective acknowledgments. Consider disabling tcp_timestamps if timestamp-based attacks are a concern.
# Disable ICMP redirect acceptance
sysctl -w net.ipv4.conf.all.accept_redirects=0
sysctl -w net.ipv4.conf.default.accept_redirects=0
# Disable source routing
sysctl -w net.ipv4.conf.all.accept_source_route=0
# Enable reverse path filtering
sysctl -w net.ipv4.conf.all.rp_filter=1
Compliance Considerations: Many compliance frameworks require network logging and monitoring. Ensure you capture and retain network flow data as required by HIPAA, PCI-DSS, or SOC 2. Implement encryption for sensitive data in transit using TLS.
Common Pitfalls / Anti-patterns
Misunderstanding Socket Buffer Sizes: Setting socket buffers too large can cause memory pressure and swapping. Setting them too small can limit throughput or increase drops. Buffer sizes should match your workload characteristics.
Ignoring TIME_WAIT: Connections in TIME_WAIT hold resources for a period after closing. High connection churn can exhaust available ports. Start by reusing connections and checking the ephemeral port range; change tcp_tw_reuse only after reviewing the kernel version and workload.
Blocking in Signal Handlers: Network operations in signal handlers can cause deadlocks or undefined behavior. Signals interrupt execution at arbitrary points, potentially while holding locks.
Assuming Order Guarantees: UDP delivers packets independently. If your application requires ordering, implement sequence numbers at the application layer.
Ignoring NIC Offloading: TCP checksum and segmentation offload can make packet captures look different from traffic on the wire. For example, a capture taken before transmit offload may show a checksum that the NIC has not calculated yet.
Quick Recap Checklist
- The Linux networking stack implements TCP/IP with modular layers
sk_buffstructures enable zero-copy packet manipulation- TCP provides reliability at the cost of latency and connection overhead
- UDP offers lower latency but no delivery guarantees
- Kernel parameters tune behavior for specific workloads
- Monitoring socket states and buffer usage reveals performance issues
- Security requires defense at multiple layers (firewall, TCP hardening, encryption)
- Production issues often involve connection exhaustion or buffer sizing
- Tools like
ss,tcpdump, andipprovide visibility into stack behavior - Understanding the stack helps debug mysterious connectivity problems
Interview Questions
The NIC transfers packet data into host memory, often using DMA, and signals the driver through an interrupt or polling mechanism such as NAPI. The driver represents the packet with an sk_buff and passes it through the protocol layers: Ethernet handling, IP processing, then TCP sequencing and flow control. TCP payload is queued for the target socket, where the application can read it.
The three-way handshake establishes a reliable connection by synchronizing sequence numbers and agreeing on initial parameters. The client sends a SYN with its initial sequence number, the server responds with SYN-ACK acknowledging the client's sequence and providing its own, and the client sends a final ACK. After this exchange, both sides have confirmed they can send and receive data reliably.
UDP avoids connection setup overhead, having no three-way handshake or teardown process. It has lower latency since there is no congestion control or retransmission delay. UDP allows broadcasting and multicasting to multiple recipients efficiently. For applications like video streaming or DNS where occasional packet loss is acceptable, UDP provides better performance.
TIME_WAIT occurs after a connection closes when the local side has sent the final ACK. It persists for two maximum segment lifetimes (typically 60 seconds) to handle delayed packets from the connection. During this time, any delayed packets from the closed connection arrive and are safely discarded rather than being misinterpreted as data for a new connection using the same port pair.
TCP uses congestion control algorithms that adjust sending rate based on network conditions. The sender maintains a congestion window that limits how much unacknowledged data can be outstanding. When packets are lost (timeout or duplicate ACKs), the window shrinks dramatically (multiplicative decrease). When ACKs arrive successfully, the window grows slowly initially (slow start) then more gradually (congestion avoidance). This approach probes for available bandwidth while backing off when congestion occurs.
netfilter is a framework in the Linux kernel that allows kernel modules to inspect, modify, and intercept network packets. It powers iptables, nftables, and ip6tables. netfilter hooks are positioned at five points in the packet processing pipeline: PRE_ROUTING (incoming packets), INPUT (packets destined for local), FORWARD (packets being forwarded), OUTPUT (locally generated packets), and POST_ROUTING (outgoing packets).
Each hook can inspect and modify packets, make routing decisions, or drop packets entirely. Firewall rules, NAT (Network Address Translation), packet logging, and connection tracking are all implemented via netfilter hooks. The iptables tool creates rules that register callback functions with these hooks — when a packet reaches a hook, the registered rules are evaluated in order.
The routing table is consulted for each outgoing packet. The kernel searches routing entries (prefix, netmask, gateway, interface) in order of specificity (longest prefix match) to find the best match for the destination IP. If a gateway is specified, the packet is forwarded to that gateway; otherwise, it is sent directly to the destination on the local network via the specified interface. The interface's ARP cache is queried to resolve the next-hop IP to a MAC address.
Multiple routing tables (policy routing) can be used: ip rule allows routing based on source IP, packet mark, or UID, selecting which table to consult. This enables complex setups like multihoming (multiple ISP connections) where different traffic types use different uplinks.
TCP and UDP checksums are 16-bit sums of the pseudo-header (source IP, destination IP, protocol, length) plus the entire segment. The checksum allows detecting data corruption during transmission. On modern NICs, checksum computation is offloaded to hardware — the NIC computes the checksum when transmitting and verifies on receive, relieving CPU overhead.
Checksum offloading affects packet capture: tcpdump sees packets before the NIC processes them, so it may see packets with incorrect or zero checksums (where the checksum is yet to be computed on transmit, or was stripped for capture). For UDP, if a received packet has a checksum error, it is silently dropped — the application never sees it. TCP is more robust to offload issues because the layer below has already validated the checksum.
As a TCP receive buffer fills, the receiver advertises a smaller window and can eventually advertise a zero window. The sender then pauses new data until the receiver reports available space; packets already in flight can still be dropped and retransmitted. UDP has no transport-level flow control or retransmission, so datagrams that arrive while its socket receive buffer is full are dropped.
Check protocol counters with netstat -s or inspect socket memory with ss -m; exact counter names vary by kernel and tool version. The SO_RCVBUFFORCE socket option (root only) can override system limits for specific sockets. Applications expecting high rates of UDP traffic should monitor drops and size receive buffers for the workload.
When an application's receive buffer is full (or nearly full), it advertises a zero window to the sender, meaning the sender should stop transmitting. The sender keeps packets in its transmit queue, waiting. The kernel tracks this per-socket. If the zero window persists for a long time, the sender may eventually timeout and retransmit.
The kernel may send a zero window probe periodically to check if space has opened up. Applications can avoid zero window stalls by: reading from the socket regularly (don't block on unrelated I/O), using non-blocking I/O with event loops that always have read capacity, and setting appropriate buffer sizes for the workload. Use ss -ti to see connection timers and window state.
SOCK_STREAM (for both AF_INET and AF_UNIX) provides a reliable, ordered, bidirectional byte stream. TCP is the most common SOCK_STREAM protocol. Messages are not preserved — writes are concatenated into a byte stream; reads may return partial or multiple writes combined. Delivery is guaranteed via acknowledgment and retransmission.
SOCK_DGRAM provides message-oriented, unreliable delivery. UDP is the primary SOCK_DGRAM protocol. Each write produces exactly one datagram; each read returns at most one write's worth of data. Datagrams may arrive out of order, duplicated, or not at all. No connection is required before sending — you directly send datagrams to a peer address.
On Linux, listen() sets the maximum length of the accept queue: connections that completed the three-way handshake and are waiting for the application to call accept(). If this queue fills, new connections may be refused or dropped, depending on kernel settings.
The backlog is capped by /proc/sys/net/core/somaxconn. The separate SYN queue holds connections whose handshakes are incomplete and is affected by settings such as tcp_max_syn_backlog. Use ss -ltn to inspect listening sockets and their queue counts; if the accept queue fills, the application may not be accepting connections quickly enough.
TCP keepalive is an option that sends a probe packet after a period of inactivity (default: 2 hours on Linux). If the peer doesn't respond, probes are sent at intervals (default: 3 probes, 75 seconds apart). After the final failure, the connection is considered dead and closed. This detects dead peers without requiring application-level heartbeats.
TCP keepalive is independent of the retransmission timeout and TIME_WAIT mechanisms. It is useful for detecting when a client machine has crashed (as opposed to cleanly closing the connection). You enable it per socket with setsockopt(SO_KEEPALIVE) and tune with tcp_keepalive_time, tcp_keepalive_probes, tcp_keepalive_intvl sysctls. Application-level keepalives (in the protocol payload) are more reliable than TCP keepalive because they are visible to the application.
TCP Fast Open (TFO) allows data to be sent in the SYN packet during the three-way handshake, eliminating one round-trip for subsequent connections to the same server. The server must have cookie support enabled (sysctl -w net.ipv4.tcp_fastopen=1 for client, =2 for server, =3 for both).
For clients: the first connection is normal; the server returns a TFO cookie. Subsequent connections to the same server send the cookie in the SYN, allowing data in the SYN. For servers: enable tcp_fastopen in the kernel and use listen(...); followed by setsockopt(IPPROTO_TCP, TCP_FASTOPEN, ...). TFO is best for short-lived connections to the same servers — repeated connections to API endpoints, for example. It can break in some middlebox scenarios.
SO_REUSEADDR allows a listening socket to bind to an address that is in TIME_WAIT (from a previous connection on that port). Without it, attempting to bind to 0.0.0.0:8080 immediately after a previous server on that port shut down would fail with EADDRINUSE. With it, the kernel allows the bind because the TIME_WAIT state is only preventing new server binds from different processes — the option tells the kernel to ignore the TIME_WAIT state for this purpose.
SO_REUSEPORT (Linux 3.9+) allows multiple processes or threads to bind to the same port, with the kernel distributing connections across them. This enables horizontal scaling of server processes without a proxy. Without SO_REUSEPORT, only one process can bind to a given port. Both options are essential for building scalable network services; always set SO_REUSEADDR before calling listen().
When an IP packet is larger than the Maximum Transmission Unit (MTU) of a network segment, it is fragmented — split into smaller IP fragments each with its own IP header and position information. The destination host reassembles the fragments. Fragmentation can happen at any hop along the path.
The kernel reassembles incoming fragments in the IP layer using a reassembly buffer. Fragmented UDP datagrams that cannot be reassembled are dropped; fragmented TCP packets are reassembled before passing to TCP. Fragment reassembly has a timeout (typically 60 seconds) and consumes memory — an attacker sending many fragments can cause memory exhaustion. Many DDoS mitigation systems drop fragments or enforce strict reassembly limits.
select() uses three fd_sets (read, write, exception) and copies them from userspace to kernel on each call. It requires re-adding all file descriptors on every call, and the number of file descriptors is limited by FD_SETSIZE (often 1024). Time complexity is O(N) per call — every call scans all tracked descriptors.
poll() is similar but uses a more flexible array of pollfd structs rather than fd_sets, removing the FD_SETSIZE limit but still requiring O(N) scanning per call. epoll() registers file descriptors once with the kernel via epoll_create()/epoll_ctl(), and epoll_wait() returns only ready descriptors. It scales to thousands of file descriptors with O(1) notification latency. For high-connection servers (web servers, proxies), epoll is the standard choice.
TCP timestamps (RFC 1323) provide two 32-bit timestamp values per segment, used for two purposes: PAWS (Protect Against Wrapped Sequence Numbers) — the timestamp acts as a logical clock, allowing the receiver to discard segments from previous incarnations of a connection (when sequence numbers wrap around). Without timestamps, a delayed segment from a previous connection could be accepted as valid. RTTM (Round Trip Time Measurement) — the timestamp echo from the peer allows accurate RTT calculation, enabling proper timeout and retransmission tuning.
Even if timestamps are not used for RTTM, PAWS protection is valuable on high-bandwidth links where sequence number wrap can happen within the MSL (Maximum Segment Lifetime). Disable timestamps only when you need the bandwidth savings or when they cause issues with certain middleboxes — but this is rare.
Inbound path: NIC generates interrupt → DMA to ring buffer → driver allocates sk_buff → protocol stack (Ethernet → IP → TCP/UDP) → socket receive buffer → application recv(). The key is that each layer strips its header and passes to the next, until data reaches the application.
Outbound path: Application send() → writes to socket send buffer → TCP/UDP encapsulation → IP routing → netfilter hooks → Ethernet framing → driver queue → NIC DMA → wire. For local processes, outbound passes through OUTPUT/netfilter hooks; for forwarded packets, it passes through FORWARD/netfilter hooks.
The important difference: inbound packets go through INPUT hooks (where iptables rules can filter them), while outbound local packets go through OUTPUT hooks. This is why iptables rules for "INPUT" affect incoming traffic to local processes, while "OUTPUT" affects locally generated outgoing traffic.
conntrack is a kernel module that tracks the state of network connections, maintaining a table of all tracked connections and their state (NEW, ESTABLISHED, RELATED, INVALID). It is the basis for stateful firewalling — instead of evaluating rules per packet, iptables can match on connection state. For example: iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT allows return traffic for established connections.
conntrack also handles NAT (Network Address Translation): when a packet passes through, conntrack replaces source/destination IP and port in both directions. This enables DNAT (port forwarding), SNAT (masquerading), and full cone NAT. High-throughput conntrack tables can become a bottleneck — use nf_conntrack_max to size the table, and monitor with conntrack -L. For IPVS (load balancing), conntrack provides connection tracking as well.
Further Reading
- Linux Kernel Networking Documentation — Official kernel docs covering network stack internals, protocol handlers, and netfilter
- TCP/IP Illustrated, Volume 1 (RFC 793, 5961, 9293) — The classic TCP specification and its modern extensions
- Sockets and Network IPC — How BSD sockets interface with the kernel networking stack
- Device Drivers Architecture — How NIC drivers interact with the kernel network stack
Conclusion
The Linux networking stack moves packets between the NIC and application sockets through the link, IP, and transport layers. Knowing where buffers, routing, netfilter, and TCP state fit helps you trace drops and latency, then choose the right tools and kernel metrics to investigate.
For user-space packet processing, explore DPDK; for programmable kernel-side processing, look at eBPF. BBR and QUIC are useful next topics when studying congestion control and transport behavior.
Category
Related Posts
ASLR & Stack Protection
Address Space Layout Randomization, stack canaries, and exploit mitigation techniques
Assembly Language Basics: Writing Code the CPU Understands
Learn to read and write simple programs in x86 and ARM assembly, understanding registers, instructions, and the art of thinking in low-level operations.
Boolean Logic & Gates
Understanding AND, OR, NOT gates and how they combine into arithmetic logic units — the building blocks of every processor.