My networking notes used to be full of layer names, header diagrams, and TCP flags. I knew the terminology, but when someone asked what happens after calling send(), my explanation stopped at "the data goes down the stack."
I changed my approach by connecting three things:
The spec: what the RFC defines.
The code: how Linux or C headers represent it.
The wire: what
tcpdumpor Wireshark captures.
This post applies that approach to four exercises, moving from packet construction to TCP security.
From application data to the network
Layer 2 vs Layer 3
One TCP header, three views
How TCP validates RST packets
The commands are generally safe on a normal Linux machine. Commands requiring elevated privileges are identified.
Part 1: From Application Data to the Network
The question
When an application sends bytes, which components add the headers, and where are those headers represented in memory?
What I found
When an application calls send() or write() on a TCP socket, the operation enters the kernel. The networking stack processes the data using structures such as sk_buff, defined in include/linux/skbuff.h.
An sk_buff can provide space before packet data (headroom) and after it (tailroom). Networking layers can use this space to add headers without necessarily copying the payload into a new buffer at every layer.
| Step | Who does it | What gets added |
|---|---|---|
| 1 | Application | Payload (your bytes) |
| 2 | TCP | TCP header, 20 bytes minimum (more with options) |
| 3 | IP | IP header, 20 bytes for IPv4 without options |
| 4 | Ethernet / neighbor layer | 14 byte Ethernet header (MACs + EtherType) |
| 5 | NIC driver | Hands the buffer to the card, usually through DMA |
| 6 | NIC hardware | Adds the FCS (CRC) and puts bits on the medium |
SA typical Ethernet frame looks like this:
[ Ethernet ][ IP ][ TCP ][ payload ][ FCS ]
The exact operations depend on the networking path and enabled features.
Why it matters
Headroom: insufficient space may require the kernel to expand or reallocate the buffer.
FCS: normal host captures often omit it because the NIC adds it after the capture point.
Offloads: checksum and segmentation offloads can make captured packets differ from their final on-wire form.
The packet diagram describes the format; the kernel and NIC determine how it is constructed and transmitted.
Part 2: Layer 2 vs Layer 3, Seen From Linux
The question
If IP addresses identify hosts, why do we also need MAC addresses?
What I found
They serve different purposes:
| Layer 2 (Ethernet) | Layer 3 (IP) | |
|---|---|---|
| Address | MAC address | IP address |
| Scope | One local link | End to end, across networks |
| Unit | Frame | Packet |
| Device | Switch | Router |
| Linux command | ip neigh |
ip route |
The via field identifies the selected next hop.
Next, inspect neighbor mappings:
ip neigh show
Example:
192.168.1.1 dev wlan0 lladdr aa:bb:cc:dd:ee:01 REACHABLE
On IPv4 Ethernet networks, ARP commonly resolves an IP address to a MAC address.
Capture the traffic to inspect both layers:
sudo tcpdump -n -e -i wlan0 host 203.0.113.20
The -e option displays link-layer information. For traffic routed through the gateway, the Ethernet destination MAC should correspond to the gateway, while the IP destination remains 203.0.113.20.
The distinction: ip route reveals the next-hop decision; ip neigh helps identify the corresponding link-layer address.
Part 3: One TCP Header, Three Views
The question
Does the TCP header diagram in a textbook correspond to what the kernel and tcpdump actually use?
View 1: The spec
The current base TCP specification is RFC 9293 (2022), which obsoleted the original RFC 793. The header format is described in section 3.1:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
| Sequence Number |
| Acknowledgment Number |
| Offset| Rsrvd | Flags | Window |
| Checksum | Urgent Pointer |
| Options (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Each row represents 32 bits. The flags are CWR ECE URG ACK PSH RST SYN FIN.
View 2: The code
On Linux systems using glibc, the TCP header is declared in <netinet/tcp.h> as struct tcphdr. A shortened representation looks like this:
struct tcphdr {
uint16_t source;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
uint16_t doff:4; /* data offset */
uint16_t syn:1;
/* ... other flag bits ... */
uint16_t window;
uint16_t check;
uint16_t urg_ptr;
};
The actual declaration includes additional flags and handles bit-field ordering according to byte order. Linux also has kernel-side TCP definitions, and other systems may use different field names.
The fields map to the RFC concepts: doff is the data offset, check the checksum, and urg_ptr the urgent pointer.
View 3: The wire
Capture TCP traffic on port 443:
sudo tcpdump -n -S -i any tcp port 443
-ndisplays numeric addresses and ports instead of resolving names.-Sdisplays absolute sequence numbers rather than relative ones.
A SYN packet might appear like this:
IP 192.0.2.10.51514 > 198.51.100.20.443: Flags [S], seq 1520943817, win 64240, options [mss 1460,...], length 0
The displayed fields can now be related to the C declaration and the RFC:
| tcpdump | struct tcphdr | RFC 9293 |
|---|---|---|
51514 |
source |
Source Port |
443 |
dest |
Destination Port |
seq 1520943817 |
seq |
Sequence Number |
Flags [S] |
syn |
SYN flag |
win 64240 |
window |
Window |
options [mss 1460] |
follows doff
|
Options |
Common flag notation includes [S] for SYN, [S.] for SYN+ACK, [P.] for PSH+ACK, [F.] for FIN+ACK, and [R] for RST.
The checksum gotcha
When I first ran tcpdump -v on the sending host, some outgoing packets appeared to have incorrect TCP checksums.
A common cause is checksum offload. The kernel may leave checksum calculation to the NIC, while the capture occurs before the final checksum is written. The packet can therefore appear invalid in the capture even though the NIC calculates the correct checksum for transmission.
You can inspect the relevant interface features with:
ethtool -k wlan0 | grep checksum
The capture point matters: captured packets may not yet reflect their final on-wire representation.
Part 4: How TCP Knows an RST Is Legitimate
The question
An RST can terminate a TCP connection. How does the receiver decide whether to accept one?
Sequence numbers as a defense
TCP tracks bytes using sequence numbers. Two relevant receiver variables are:
RCV.NXT: the next sequence number expected.RCV.WND: the receive window, describing the currently acceptable sequence-number range.
Under the original RFC 793 rules, an RST could be accepted if its sequence number fell within the receive window. A blind attacker therefore did not necessarily need to guess the exact expected number.
RFC 5961 tightened the check
RFC 5961, section 3, introduced stricter RST validation. Suppose RCV.NXT = 5000 and the receive window is 1000.
| RST seq | Where it falls | What the receiver does |
|---|---|---|
5000 |
Exactly RCV.NXT
|
Reset the connection |
5400 |
Inside the window, not exact | Send a challenge ACK, keep the connection |
9000 |
Outside the window | Drop it silently |
Instead of immediately accepting an in-window RST with a non-exact sequence number, the receiver sends a challenge ACK reflecting its current TCP state.
This makes blind, off-path reset attacks harder because the attacker must produce a more precise packet without observing the connection. The sequence-number space contains (2^{32}), or about 4.29 billion, possible values, although real security also depends on protocol state, window size, implementation details, and the attacker's information.
Checking it on Linux
Linux implements challenge-ACK handling. You can inspect the corresponding counter with:
nstat -az TcpExtTCPChallengeACK
Related settings include:
sysctl net.ipv4.tcp_challenge_ack_limit
sysctl net.ipv4.tcp_invalid_ratelimit
CVE-2016-5696 demonstrated that Linux's shared challenge-ACK rate limit could leak information useful for inferring TCP connection state. Subsequent kernel changes addressed this side channel by changing how the limit was applied.
This illustrates why security mechanisms must also be assessed for information leaks introduced by their implementations.
What to look for in Wireshark
Filter resets with
tcp.flags.reset == 1.Disable relative sequence numbers or add
tcp.seq_rawto inspect raw values.Compare the RST sequence number with the receiver's expected sequence.
Look for a challenge ACK following an in-window RST that fails the stricter check. Wireshark's analysis labels depend on the observed exchange.
An RST followed by an ACK while the connection continues may be consistent with challenge-ACK handling, but the surrounding packets are needed to establish what happened.
Blind vs on-path attackers
Blind (off-path): cannot observe the connection's traffic and must infer or guess sequence numbers. RFC 5961 makes certain blind reset attacks significantly harder.
On-path: can observe traffic and may know the relevant sequence numbers. Sequence-number checks alone do not authenticate against this attacker.
For deployments needing TCP-level authentication, TCP Authentication Option (TCP-AO), defined in RFC 5925, can authenticate segments using a shared key. A forged RST lacking the required authentication value should fail validation when TCP-AO is correctly configured and enforced.
TCP-AO succeeds the older TCP MD5 signature option (RFC 2385), is relevant to sessions such as BGP, and gained Linux kernel support in version 6.7.
TLS does not solve this problem: TCP handles connection resets below the TLS layer, so TLS alone does not authenticate TCP RST packets.
Key idea
Sequence-number checks help defend against blind attackers. When protection against attackers who can observe the traffic is required, TCP-level authentication, such as TCP-AO where supported and configured, addresses a different part of the threat model.
What Changed in How I Learn
These exercises exposed gaps in my earlier notes. I knew an RST terminates a connection, but had not investigated how TCP decides to accept one. I understood the difference between IP and MAC addresses, but had not examined a packet where the two destinations referred to different machines.
The method is straightforward:
Read the RFC: focus on the relevant section.
Find the implementation: inspect a C header, kernel source file, or sysctl.
Observe the behavior: use
tcpdump, Wireshark,ip,ss, ornstat.Investigate differences: unexpected output can reveal details about the implementation or capture point.
Connecting the specification, implementation, and observed traffic gives you a practical way to test and deepen your understanding.
References
RFC 9293: Transmission Control Protocol (TCP)
RFC 5961: Improving TCP's Robustness to Blind In-Window Attacks
RFC 5925: The TCP Authentication Option
RFC 2385: Protection of BGP Sessions via the TCP MD5 Signature Option
Linux source:
include/linux/skbuff.h,net/ipv4/tcp_input.cglibc:
<netinet/tcp.h>man 8 ip-route,man 8 ip-neighbour,man 8 tcpdump,man 8 nstat




Top comments (0)