DEV Community

Cover image for Learning TCP/IP From the Linux Side: Spec, Code, and Wire
Musa Nayyer
Musa Nayyer

Posted on Originally published at muzasio.hashnode.dev AI-assisted

Learning TCP/IP From the Linux Side: Spec, Code, and Wire

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:

  1. The spec: what the RFC defines.

  2. The code: how Linux or C headers represent it.

  3. The wire: what tcpdump or Wireshark captures.

This post applies that approach to four exercises, moving from packet construction to TCP security.

  1. From application data to the network

  2. Layer 2 vs Layer 3

  3. One TCP header, three views

  4. 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

network overview inside application

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 ]
Enter fullscreen mode Exit fullscreen mode

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

layer 2 vs layer 3

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
Enter fullscreen mode Exit fullscreen mode

Example:

192.168.1.1 dev wlan0 lladdr aa:bb:cc:dd:ee:01 REACHABLE
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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)                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • -n displays numeric addresses and ports instead of resolving names.

  • -S displays 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The capture point matters: captured packets may not yet reflect their final on-wire representation.


Part 4: How TCP Knows an RST Is Legitimate

RST

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
Enter fullscreen mode Exit fullscreen mode

Related settings include:

sysctl net.ipv4.tcp_challenge_ack_limit
sysctl net.ipv4.tcp_invalid_ratelimit
Enter fullscreen mode Exit fullscreen mode

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_raw to 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:

  1. Read the RFC: focus on the relevant section.

  2. Find the implementation: inspect a C header, kernel source file, or sysctl.

  3. Observe the behavior: use tcpdump, Wireshark, ip, ss, or nstat.

  4. 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.c

  • glibc: <netinet/tcp.h>

  • man 8 ip-route, man 8 ip-neighbour, man 8 tcpdump, man 8 nstat

Top comments (0)