Reading an RFC Header Diagram
Almost every protocol header you will ever meet is specified by a picture: a grid of
+-+-+separators under a two-line ruler, with field names written inside the boxes. That picture is not an illustration of the specification — it is the specification, precise to the individual bit. RFC 791 (Postel, September 1981) fixed the reading rules for it in a single page of Appendix B: octets are transmitted in the order you read English, the leftmost bit of a numeric field is its most significant bit, and the bit labelled0is therefore the high-order bit — the exact opposite of how most programming languages number bits. Learn the ruler once and forty later diagrams become legible; misread it once and you will build a parser that is off by a byte on every packet. This note teaches the notation itself: how to count a field’s width from the separators, why the grid is 32 bits wide (except when it is not), what the notation deliberately does not tell you, and how the same layout is transcribed into a mermaidpacket-betadiagram — the medium this vault uses for the rest of the Protocol Wire Formats and Headers MOC.
Mental Model: The Diagram Is a Coordinate System
The best way to think about an RFC header diagram is as a coordinate system laid over a byte stream. The bytes arrive one after another, in a strictly defined order; the diagram is a ruler you hold up against that stream to say “bits 16 through 31 of this header are the Total Length field.” Every element of the notation exists to make one of two statements: where does this field start and end, and which end of it is the big end.
That means the diagram answers exactly three questions, and refuses to answer a fourth:
flowchart TB D["An RFC header diagram<br/>the +-+-+ grid under a bit ruler"] D --> Q1["<b>Where?</b><br/>the bit offset of each field<br/>from the start of the header"] D --> Q2["<b>How wide?</b><br/>the field's size in bits,<br/>counted from the separators"] D --> Q3["<b>Which end is big?</b><br/>leftmost bit = most significant<br/>(RFC 791 Appendix B)"] D -.->|"deliberately silent"| Q4["<b>What does it mean?</b><br/>units, legal values, defaults,<br/>variable-length rules<br/>→ the prose beneath the figure"] Q1 --> P["A parse instruction:<br/>read N bits at offset K,<br/>interpret big-endian"] Q2 --> P Q3 --> P Q4 -.-> P style Q4 stroke-dasharray: 5 5
What it shows: the three things a header diagram encodes and the one thing it does not. The insight to take: the picture is a machine-checkable statement about offsets and widths, and nothing more. Everything semantic — that IPv4’s IHL counts 32-bit words rather than octets, that a UDP checksum of zero means “not computed” — lives in the prose under the figure. Reading only the picture gets you a syntactically correct parser that is semantically wrong, which is the single most common way protocol code goes subtly bad.
The corollary matters when you go to draw a header rather than read one: any notation that preserves offsets, widths and bit significance is an equally valid rendering. An ASCII grid, a mermaid packet-beta fence and a table of (field, offset, width) triples all carry identical information. That is why this vault can transcribe RFC figures into mermaid without losing anything — and why a sloppy transcription that shifts one offset silently corrupts the specification.
The Ruler, Digit by Digit
Here is the header of the ruler that opens essentially every diagram in the classic RFC style, taken verbatim from RFC 791 Appendix B:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 1 | 2 | 3 | 4 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 5 | 6 | 7 | 8 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 9 | 10 | 11 | 12 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
RFC 791 Figure 10, “Transmission Order of Bytes” — the twelve boxes are numbered in the order the octets go on the wire. This one is reproduced as an ASCII fence rather than mermaid deliberately: the subject of this note is the notation itself, so the notation has to appear in its own form at least once. The insight to take: transmission order is simply reading order — left to right, then top to bottom, exactly as RFC 791 puts it, “the normal order in which they are read in English.”
The ruler is two decimal digits stacked vertically, which is the part that trips people up on first contact. The top line counts tens: the 1 sits above bit 10, the 2 above bit 20, the 3 above bit 30. The bottom line counts units, cycling 0 1 2 3 4 5 6 7 8 9 four times over. Read them as a column and you get the bit index: the column with 2 on top and 7 below is bit 27. Columns with a blank top line are bits 0 through 9. Every tick mark in the separator row corresponds to exactly one bit position — RFC 9293 still repeats that reminder under its own figure, in the caption line noting that one tick mark represents one bit position.
A row is therefore 32 bits = 4 octets = one 32-bit word, and a header of n rows is 4n octets long. The IPv4 header’s minimum size of 20 octets is exactly the five rows its diagram draws before options begin.
Bit 0 Is the Most Significant Bit
This is the rule that contradicts programming intuition, and RFC 791 states it flatly: whenever an octet represents a numeric quantity, the leftmost bit in the diagram is the high-order or most significant bit — “the bit labeled 0 is the most significant bit.” The RFC illustrates it with a single octet drawn as 1 0 1 0 1 0 1 0 across positions 0–7 and labels its value 170 decimal (RFC 791 Figure 11). Work it out: if bit 0 were the least significant bit, that same pattern would read 85. The specification says 170, so bit 0 carries the weight 2⁷.
The rule extends past the octet boundary. RFC 791 continues: for a multi-octet field, the leftmost bit of the whole field is the most significant bit, and when a multi-octet quantity is transmitted, the most significant octet goes first. RFC 1700 (Reynolds and Postel, October 1994, STD 2) restates the same paragraphs and gives them their name, saying the convention in Internet protocol documentation is to picture data in “big-endian” order, citing Danny Cohen’s IEN 137. The byte-order half of that story is the subject of Network Byte Order and Endianness on the Wire; what matters here is the diagram-reading consequence.
packet-beta 0-7: "octet 1 (transmitted first, most significant)" 8-15: "octet 2" 16-23: "octet 3" 24-31: "octet 4 (transmitted last, least significant)"
A single 32-bit field spread across four octets, drawn as a mermaid packet-beta diagram. What it shows: the leftmost box is both the first octet on the wire and the highest-weighted octet of the number. The insight to take: “first” and “most significant” are the same direction in this notation — which is precisely why the notation is called big-endian, and precisely why little-endian host code has to swap before writing and after reading.
Two immediate practical consequences follow.
A bit index in a diagram is not a shift amount. If a flag sits at diagram bit 3 of an octet, the C test is byte & 0x10 (weight 2⁴), not byte & (1 << 3). The conversion is weight = 1 << (7 - diagram_bit) within an octet. Getting this backwards produces code that compiles, runs, and reads the wrong flag.
C bitfields do not lay out in diagram order. The Linux kernel makes this explicit and unavoidable. In include/uapi/linux/ip.h the first octet of the IPv4 header is declared twice:
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Please fix <asm/byteorder.h>"
#endifLine by line: the diagram says version occupies bits 0–3 and ihl bits 4–7, so version is the high nibble. On a big-endian-bitfield compiler the declaration order matches the diagram. On a little-endian-bitfield compiler the first-declared member lands in the low nibble, so the kernel must reverse the declaration order to describe the same wire bytes. The #error arm exists because a compiler that declares neither cannot be trusted to lay the struct out at all. Read that snippet as a warning label: the diagram’s left-to-right order is a wire fact, and your language’s bitfield order is a compiler fact, and they are different facts.
Counting a Field’s Width from the Separators
The mechanical skill is measuring widths off the grid without trusting a label. Take the first word of the IPv4 header (RFC 791 §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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The counting procedure has three steps, and it is worth doing consciously the first few times.
- Find the vertical bars. They fall between bit columns. Here they sit before bit 0, between bits 3 and 4, between 7 and 8, between 15 and 16, and after bit 31.
- Read the span between consecutive bars off the ruler.
Versionspans columns 0–3;IHLspans 4–7;Type of Servicespans 8–15;Total Lengthspans 16–31. - Width is
end − start + 1. So 4, 4, 8 and 16 bits — summing to 32, which is the arithmetic check that you have not miscounted. Every full row must sum to the row width; if it does not, you dropped a column.
That last check is the one to internalise, because the failure it catches — an off-by-one in a field boundary — is exactly the class of error that turns into a parser vulnerability when a length field is involved. The same word rendered in this vault’s house medium:
packet-beta 0-3: "Version" 4-7: "IHL" 8-15: "Type of Service" 16-31: "Total Length"
The first 32-bit word of the IPv4 header as mermaid packet-beta (RFC 791 §3.1). What it shows: identical offsets and widths to the ASCII grid above, in a form that renders natively in Obsidian. The insight to take: the transcription is lossless because both notations encode the same three facts — start, end, and left-is-most-significant. The full thirteen-field walk-through of this header, with what each field means, belongs to The Internet Protocol Version 4 Header Field by Field; this note only teaches the reading of the grid.
Two habits are worth forming alongside the counting. First, a field drawn as spanning a full row is not necessarily aligned to anything meaningful — IPv4’s Fragment Offset is 13 bits precisely because three flag bits share its row. Second, the diagram routinely lies about variable-length regions: a row labelled Options or Data with a fixed height is drawing a variable extent, and only the prose tells you how far it actually runs. Sometimes the notation admits this with colon characters instead of bars, as RFC 9293 does for the TCP payload rows.
The Notation Is Not Universal — A Field Guide
It is tempting to assume every RFC draws headers the same way. It does not, and the differences are not cosmetic. The following are all verified by reading the specifications directly.
| Specification | Notation | Ruler | Gotcha when reading |
|---|---|---|---|
| RFC 791 (IPv4, 1981), RFC 9293 (TCP, 2022), RFC 8200 (IPv6, 2017) | ASCII grid, +-+-+ separators | Two-line, 32 bits, one tick per bit | The canonical form — everything below is a deviation |
| RFC 768 (UDP, August 1980) | ASCII grid, +--------+ separators | Single line marking octet boundaries: 0 7 8 15 16 23 24 31 | Predates RFC 791’s convention by a year; ticks are octets, not bits |
| RFC 1035 (DNS, 1987) | ASCII grid, +--+--+ separators | 16 bits wide, with the tens digit written on the line above | A row is two octets, not four — mentally halve every offset |
| RFC 826 (ARP, 1982) | No diagram at all — a prose list of 16.bit: (ar$hrd) … entries | None | Widths are in the text; the header is variable-length by construction |
| RFC 8446 (TLS 1.3, 2018) | C-like presentation language (uint8, uint16, vectors with length prefixes) | None | §3.1 defines the byte-concatenation rule instead of drawing it |
| RFC 9000 (QUIC, 2021) | Textual structure notation, Field Name (8) giving the width in bits | None | §17 states all values are network byte order and fields start from the high-order bits of each byte |
The notation zoo, each row verified against the specification’s own text. The insight to take: the 32-bit ASCII grid is a dominant convention, not a universal one, and the two most recently designed transports in wide use — TLS 1.3 and QUIC — abandoned it entirely for textual notations that a machine can parse. When you open an unfamiliar RFC, the first thing to identify is which of these six shapes you are looking at; assuming a 32-bit ruler over RFC 1035’s 16-bit one will double every offset you compute.
RFC 768’s UDP figure deserves a second look because it is a genuine trap. Its ruler reads 0 7 8 15 16 23 24 31, and the boxes are drawn two text-lines tall so the field names fit. A reader who pattern-matches it against RFC 791’s grid and counts boxes rather than reading the numbers will conclude the header is four octets rather than eight. The numbers are authoritative; the box art is not.
RFC 1035’s DNS header is the mirror-image trap. Its ruler is written as a column of digits — a line reading 1 1 1 1 1 1 above 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 — so the row runs bits 0 through 15 only. Its second row packs QR, a 4-bit Opcode, AA, TC, RD, RA, a 3-bit Z and a 4-bit RCODE into sixteen bits, and the sum check (1+4+1+1+1+1+3+4 = 16) is how you confirm you read it correctly.
From ASCII Grid to packet-beta
Mermaid gained a packet diagram type in v11.0.0 (mermaid.js.org), built for exactly this job. The transcription rule is mechanical: every field: start–end triple you counted off the grid becomes one line of the form start-end: "Field Name". Nothing else changes. Bit numbering, significance and transmission order carry over unaltered, because packet-beta inherits the same left-is-first convention.
flowchart LR A["RFC ASCII grid<br/>+-+-+ separators<br/>two-line bit ruler"] --> B["Count separators<br/>read spans off the ruler"] B --> C["(field, start, end)<br/>triples"] C --> D["<code>start-end: "Field"</code><br/>one line per field"] C --> E["markdown table<br/>field / offset / width"] D --> F["mermaid <code>packet-beta</code> fence<br/>renders natively in Obsidian"] C -.->|"row sums must equal 32"| B D -.->|"parser rejects gaps<br/>and overlaps"| F
The transcription pipeline from an RFC figure to this vault’s house diagram format. The insight to take: the intermediate (field, start, end) representation is the real content; ASCII, mermaid and a table are three serialisations of it. The two dashed feedback arrows are the safety net — your own row-sum arithmetic on the way in, and mermaid’s parser on the way out.
That second dashed arrow is the reason packet-beta beats hand-drawn box art, and it is worth being concrete about. Mermaid’s packet parser tracks the last consumed bit and validates every block against it. Reading the source, it raises errors in three distinct cases: an end offset less than its start (“End must be greater than start”), a block that does not begin exactly one bit after the previous block ended (“is not contiguous. It should start from …”), and a zero-bit field. In other words gaps and overlaps are hard errors, not silent rendering artefacts. You cannot accidentally draw a 15-bit field where the spec says 16 — the diagram simply fails to render. An ASCII grid, by contrast, will happily display a miscounted field forever.
Two syntactic forms exist. The original is the explicit bit range; since v11.7.0 mermaid also accepts a bit count written +16: "Source Port", which auto-advances the offset from the previous field. The count form is easier to edit — inserting a field does not require renumbering everything after it — but it depends on a newer renderer, so this vault’s house style is the explicit range form, which works on every version that supports packet diagrams at all.
The diagram keyword itself has two spellings. The type was introduced as packet-beta; upstream documentation now writes it as plain packet. The detector source matches the regular expression /^\s*packet(-beta)?/, so both parse — and packet-beta is the safer choice against an older bundled renderer, which is why every diagram in this MOC uses it.
| Knob | Default | What it does |
|---|---|---|
bitsPerRow | 32 | Bits drawn per row. Set to 16 to mirror RFC 1035’s DNS layout, or 64 for a wide structure. |
bitWidth | 32 | Pixel width allotted to each bit. |
rowHeight | 32 | Pixel height of a row. |
showBits | true | Whether the bit-index ruler is drawn at all. Leave it on — the ruler is the point. |
Packet-diagram configuration defaults, read from mermaid’s config schema. The insight to take: bitsPerRow is the one that matters for fidelity — a header specified on a 16-bit ruler should be redrawn on a 16-bit ruler, or the visual shape of the spec is lost even though the offsets stay correct.
A field that runs past a row boundary is split automatically across rows by the renderer, which is how a 32-bit sequence number appears as one full row and how a 128-bit IPv6 address appears as four.
Failure Modes and Common Misunderstandings
Copying a diagram from a secondary source instead of the RFC. Mermaid’s own documentation ships a TCP header example — and it is the pre-ECN layout. It allocates 100-105 to Reserved and then six flag bits URG through FIN, which is the original RFC 793 shape. RFC 9293 §3.1 (August 2022, STD 7) draws four reserved bits and eight control bits — CWR, ECE, URG, ACK, PSH, RST, SYN, FIN — because two former reserved bits were taken for explicit congestion notification. Both facts here were checked by reading the mermaid docs and the RFC text side by side. The lesson generalises: transcribe from the specification, never from another drawing.
packet-beta 0-3: "Data Offset" 4-7: "Rsrvd" 8: "CWR" 9: "ECE" 10: "URG" 11: "ACK" 12: "PSH" 13: "RST" 14: "SYN" 15: "FIN"
The third word’s second half of the TCP header as RFC 9293 §3.1 actually specifies it, drawn on a 16-bit ruler for legibility (these are header bits 96–111). The insight to take: four reserved bits and eight flags, not six and six. The meaning of each flag, and which combinations you see in a capture, belong to TCP Control Bits and Flag Combinations — this is purely the shape.
Treating a bit index as a shift count. Covered above; the conversion within an octet is 1 << (7 − diagram_bit). This bites hardest on single-bit flags, where the wrong mask silently reads a neighbouring flag rather than crashing.
Assuming the diagram’s units. The picture tells you a field is 4 bits wide; it does not tell you that IPv4’s IHL counts 32-bit words (so a minimum legal value of 5 means 20 octets), that IPv4’s Fragment Offset counts 8-octet units, or that TLS record lengths count plain octets. Units live in prose. Header Length Fields and Variable-Length Option Encoding is this MOC’s dedicated treatment of exactly that hazard.
Trusting the picture when it disagrees with the text. This is common enough that an IETF draft was written about it. draft-mcquistin-augmented-ascii-diagrams (McQuistin, Band, Jacob and Perkins, University of Glasgow; version 10 dated March 2022, now expired) catalogues real inconsistencies in published RFCs: field names that differ between a diagram and the prose describing it, and at least one case where a diagram’s widths and the accompanying text’s widths flatly contradict each other. The draft’s proposal is an augmented notation — machine-readable field definitions with widths, constraints and presence conditions — so parsers can be generated from the specification rather than hand-written against it. Practically: when a figure and its prose disagree, escalate rather than guess, and check the RFC’s errata.
Assuming the ruler is 32 bits. See the field guide above. DNS is the common trip-hazard.
Forgetting that the diagram is a header, not a packet. Encapsulation means the bytes before your header belong to a lower layer and shift every absolute offset. A tcpdump byte-offset filter like ip[0] & 0xf is written against layer-relative offsets precisely so you do not have to do that arithmetic by hand; the mechanics live in The pcap Capture Filter Language, and the layering itself in Encapsulation and Protocol Layering.
Alternatives to the ASCII Grid
The ASCII grid persisted for four decades because it is plain text: it survives email, diffs, terminals and reprints without a toolchain. Its costs are equally real — it is unparseable by machine, invisible to screen readers, and cannot express conditionality (“this field is present only if bit X is set”).
Three successors exist and none has displaced it. SVG line art was blessed for RFCs by RFC 7996 (Brownlee, December 2016), which defines a restricted “SVG 1.2 RFC” profile for the v3 RFC format — deliberately monochrome (only black and white are permitted), with multimedia, scripting, animation and interactivity excluded, so that figures render on small screens and stay stable after publication. It permits title and desc elements for accessibility, while candidly noting that reading out the SVG of a complex diagram is unlikely to help a listener much. Augmented ASCII diagrams, the Glasgow draft above, keep the human-readable picture and add machine-readable structure around it. Presentation languages — TLS’s C-like syntax and QUIC’s structure notation — skip pictures altogether in favour of text a parser generator can consume; the trade is that you lose the at-a-glance sense of a header’s shape that a grid gives you for free.
For this vault the choice is settled: mermaid packet-beta for byte-level layouts, a markdown table when the facts are enumerable and a picture would add nothing, an ASCII fence only when the RFC’s own notation is the subject (as in this note), and a static image never. The reasoning is in Drawing Wire Formats with Mermaid Packet Diagrams.
Production Notes
The reading skill taught here is not academic — it is the daily working loop of anyone who debugs a network. Three places it shows up directly:
Writing a capture filter is reading a diagram. A pcap expression such as ip[0] & 0xf != 5 selects IPv4 packets whose header is longer than five words, i.e. packets carrying options. Deriving that expression requires exactly the counting procedure from earlier: IHL occupies diagram bits 4–7 of octet 0, so the mask is the low nibble, 0x0f. The full grammar is The pcap Capture Filter Language.
Reviewing a struct definition against a spec. Any struct that claims to overlay a header needs checking field by field against the diagram — widths, order, and whether the compiler’s bitfield layout matches the wire, which as shown above it may not without explicit #if guards. Endianness-annotated types (__be16, __be32 in Linux) exist to let a static checker catch the other half of the problem; see Network Byte Order and Endianness on the Wire.
Checksum code is order-insensitive in a way that surprises people. RFC 1071 §1.2 (Braden, Borman and Partridge, September 1988) proves that the Internet checksum’s one’s-complement sum can be computed in either byte order — swapping bytes consistently rotates the bits within the sum without disturbing their internal ordering — so a little-endian host summing big-endian data gets a byte-swapped sum that swaps back when stored. It is a genuinely useful property and a genuinely common source of confusion when someone tries to “fix” working checksum code that appears to ignore endianness. The arithmetic is walked in Internet Checksum Computation.
Finally, a point-in-time caveat worth carrying: the diagrams themselves are stable, but which RFC is authoritative is not. TCP’s header diagram now lives in RFC 9293 (2022), not RFC 793 (1981); IPv6’s in RFC 8200 (2017), not RFC 2460. A diagram copied from a superseded document may be a decade out of date while looking perfectly authoritative. Check the obsoletes chain before trusting any figure — Specification Versioning and Obsoleted RFCs would be the place to formalise that habit, and the current pinning for this MOC is listed in Protocol Wire Formats and Headers MOC.
Uncertain
Verify: that mermaid’s packet-diagram parser rejects overlapping ranges as well as gaps. The source read on 2026-07-21 raises “is not contiguous. It should start from N” whenever a block’s start is not exactly
lastBit + 1, which by construction catches an overlap too — but this was inferred from readingparser.tsrather than from documented behaviour or an executed test. Reason: not directly addressed in mermaid’s user documentation. To resolve: paste a fence containing0-15: "A"followed by8-23: "B"into Obsidian and confirm it errors rather than rendering. Also verify: the claim that mermaid’s documented TCP example reflects RFC 793’s original six-reserved-bit layout specifically — the layout was verified against RFC 9293 and found to differ, but RFC 793’s own text was not fetched during this task, and the two ECN bits were introduced by RFC 3168 (2001), which was likewise not read here. To resolve: read RFC 793 §3.1 and RFC 3168 §6.#uncertain
See Also
- THE OTHER HALF OF §1: Network Byte Order and Endianness on the Wire — the diagram says the leftmost octet goes first; this is what that costs a little-endian host, and the conversion functions that pay it.
- SIBLINGS in §1: Header Length Fields and Variable-Length Option Encoding (what the widths mean — words, octets, 8-octet units), Internet Checksum Computation, Drawing Wire Formats with Mermaid Packet Diagrams (the house style this note’s transcription rule feeds).
- THE DIAGRAMS THIS NOTE UNLOCKS: The Internet Protocol Version 4 Header Field by Field, The Transmission Control Protocol Header Field by Field, The User Datagram Protocol Header Layout, The Internet Protocol Version 6 Header and Extension Headers, The DNS Message Format.
- WHERE READING BECOMES DOING: The pcap Capture Filter Language, Mapping a Capture Back to the Header Diagrams.
- THE MECHANISM BEHIND THE BYTES: Encapsulation and Protocol Layering, The OSI and TCP-IP Reference Models, and the complete Networking and Protocols MOC for what every field is for.
- PARENT: Protocol Wire Formats and Headers MOC — §1, How to Read a Wire Format.