# 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

![](https://cdn.hashnode.com/uploads/covers/69ad926686766ac3a63c2939/f7e7ce0b-dd36-42a0-b914-f72a43c7a592.png align="center")

### 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:

```text
[ 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

![](https://cdn.hashnode.com/uploads/covers/69ad926686766ac3a63c2939/fd07d3b5-99c3-4c6d-893f-9d68b60b1983.png align="center")

### 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:

```bash
ip neigh show
```

Example:

```text
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:

```bash
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

![](https://cdn.hashnode.com/uploads/covers/69ad926686766ac3a63c2939/b7ba7565-563f-4735-a618-61c08d5e4fc6.png align="center")

### 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:

```plaintext
 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:

```c
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:

```bash
sudo tcpdump -n -S -i any tcp port 443
```

*   `-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:

```plaintext
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:

```bash
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

![](https://cdn.hashnode.com/uploads/covers/69ad926686766ac3a63c2939/9e78025b-a702-4864-b5c5-721d5c550053.png align="center")

### 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:

```bash
nstat -az TcpExtTCPChallengeACK
```

Related settings include:

```bash
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_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`
