Walldye

Packet header

An IPv4 packet starts with a header of at least 20 bytes, drawn in RFC 791 as 32-bit rows.

Made with Claude Opus 5.5

Technique
text characters, technical drawing
Shape
Any screen
Added
27 September 2026

Versions

Export

Shape
cropped from 16:9
Crop
Size
Format

Colors

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Notes

Every router that forwards an IPv4 packet lowers its Time to Live by at least one and destroys the packet when the count reaches zero, so a packet caught in a routing loop cannot circle forever. The diagram is copied character for character from Figure 4 of the 1981 specification.

Sources

  1. Jon Postel, RFC 791: Internet Protocol, 1981.
  2. Jon Postel, RFC 793: Transmission Control Protocol, 1981.
  3. Jon Postel, RFC 768: User Datagram Protocol, 1980.
  4. Paul Mockapetris, RFC 1035: Domain Names - Implementation and Specification, 1987.

Source code

wallpapers/packet-header/design.py, 233 lines

"""Header diagrams from early RFCs in Spleen pixel type, with one field lit and a short note beside it."""

from dataclasses import dataclass
from typing import Literal

from walldye import (
    ACCENT,
    ACCENT_3,
    BG_ALT,
    UI,
    UI_ALT,
    UI_HI,
    Canvas,
    Color,
    Params,
    design,
    knob,
    mix,
)
from walldye.pixel import glyphs

# The diagrams are copied character for character from the RFC text, less their indent.
SEP = "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"
BITS = [
    " 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",
]
IP = [  # RFC 791, Figure 4
    *BITS,
    SEP,
    "|Version|  IHL  |Type of Service|          Total Length         |",
    SEP,
    "|         Identification        |Flags|      Fragment Offset    |",
    SEP,
    "|  Time to Live |    Protocol   |         Header Checksum       |",
    SEP,
    "|                       Source Address                          |",
    SEP,
    "|                    Destination Address                        |",
    SEP,
    "|                    Options                    |    Padding    |",
    SEP,
]
TCP = [  # RFC 793, Figure 3
    *BITS,
    SEP,
    "|          Source Port          |       Destination Port        |",
    SEP,
    "|                        Sequence Number                        |",
    SEP,
    "|                    Acknowledgment Number                      |",
    SEP,
    "|  Data |           |U|A|P|R|S|F|                               |",
    "| Offset| Reserved  |R|C|S|S|Y|I|            Window             |",
    "|       |           |G|K|H|T|N|N|                               |",
    SEP,
    "|           Checksum            |         Urgent Pointer        |",
    SEP,
    "|                    Options                    |    Padding    |",
    SEP,
    "|                             data                              |",
    SEP,
]
OCTETS = "+--------+--------+--------+--------+"
UDP = [  # RFC 768
    " 0      7 8     15 16    23 24    31",
    OCTETS,
    "|     Source      |   Destination   |",
    "|      Port       |      Port       |",
    OCTETS,
    "|                 |                 |",
    "|     Length      |    Checksum     |",
    OCTETS,
    "|",
    "|          data octets ...",
    "+---------------- ...",
]
SEP16 = "+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+"
DNS = [  # RFC 1035, 4.1.1
    "                                1  1  1  1  1  1",
    "  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5",
    SEP16,
    "|                      ID                       |",
    SEP16,
    "|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |",
    SEP16,
    "|                    QDCOUNT                    |",
    SEP16,
    "|                    ANCOUNT                    |",
    SEP16,
    "|                    NSCOUNT                    |",
    SEP16,
    "|                    ARCOUNT                    |",
    SEP16,
]
PX = 2
CW, CH = 8 * PX, 16 * PX  # one 8x16 character cell
DIM, RULE = mix(BG_ALT, UI, 0.5), mix(BG_ALT, UI, 0.75)
TEXT = mix(UI_ALT, UI_HI, 0.3)

Name = Literal["ip", "tcp", "udp", "dns"]


class Header(Params):
    figure: Name = knob(default="ip", doc="which RFC diagram")


VARIANTS = {"tcp": Header(figure="tcp"), "udp": Header(figure="udp"), "dns": Header(figure="dns")}


@dataclass(frozen=True)
class Figure:
    title: str  # "" for none
    lines: list[str]
    caption: str  # "" for none
    ruler: int  # rows of bit numbers above the first rule
    rows: range  # the lit field, its rules included
    cols: range
    note: str
    lit: range  # the lit characters of the note
    fx: float  # the figure's left edge on a landscape screen, as a fraction of the width
    right: bool = False  # the note sits right of the figure on a landscape screen


def figure(name: Name) -> Figure:
    """The diagram `name`, the field it lights and the note beside that field."""
    match name:
        case "ip":  # a router lowers the Time to Live by one: one hop
            return Figure(
                title="3.1.  Internet Header Format",
                lines=IP,
                caption="Figure 4.",
                ruler=2,
                rows=range(6, 9),
                cols=range(17),
                note="64 -> 63",
                lit=range(6, 8),
                fx=0.3125,
            )
        case "tcp":  # SYN on the first segment of the RFC's handshake example, its Figure 7
            return Figure(
                title="TCP Header Format",
                lines=TCP,
                caption="Figure 3.",
                ruler=2,
                rows=range(8, 13),
                cols=range(28, 31),
                note="<SEQ=100><CTL=SYN>",
                lit=range(14, 17),
                fx=0.3125,
            )
        case "udp":  # port 53, where RFC 1035 puts name servers
            return Figure(
                title="",
                lines=UDP,
                caption="User Datagram Header Format",
                ruler=1,
                rows=range(1, 5),
                cols=range(18, 37),
                note="53",
                lit=range(2),
                fx=0.5,
                right=True,
            )
        case "dns":  # QR is 0 in a query and 1 in its response
            return Figure(
                title="4.1.1. Header section format",
                lines=DNS,
                caption="",
                ruler=2,
                rows=range(4, 7),
                cols=range(4),
                note="0 -> 1",
                lit=range(5, 6),
                fx=0.36,
            )


@design(aspects="any", variants=VARIANTS)
def draw(s: Canvas[Header]) -> None:
    f = figure(s.params.figure)
    width = max(len(line) for line in f.lines)
    # Rows run from the title to past the caption.
    top = -2 if f.title else 0
    bottom = len(f.lines) + 2 if f.caption else len(f.lines)
    detail = bottom + 1
    mid = len(f.rows) // 2  # the field's middle row, where the note sits
    left = 0 if f.right else len(f.note) + 1  # note columns left of the figure
    beside = (width + len(f.note) + 1) * CW  # the figure with the note in its margin
    # The note sits in the margin beside the lit row. The 32-bit rows leave only 40 px on a
    # portrait screen, so there the figure is centered and the lit field is repeated under it
    # (from row `detail`), with the note beside that.
    zoom = not s.landscape and beside > s.w - 160
    at = s.pick(landscape=(f.fx, 0.5), portrait=(0.5, 0.46), snap=PX)
    if s.landscape:
        reserve = width + len(f.note) + 1 - left
        x0 = min(at.x, s.w - reserve * CW - 240)  # 16:10 would leave too little on the right
    elif zoom:
        x0 = at.x - width * CW / 2
    else:
        x0 = at.x - beside / 2 + left * CW
    if zoom:
        y0 = at.y - (top + detail + len(f.rows) + 1) / 2 * CH
        note = (x0 + (f.cols.stop + 1) * CW, y0 + (detail + mid) * CH)
    else:
        y0 = at.y - (top + bottom) / 2 * CH
        nx = x0 + (width + 1) * CW if f.right else x0 - left * CW
        note = (nx, y0 + (f.rows.start + mid) * CH)

    def color(c: int, r: int, ch: str) -> Color:
        lit = c in f.cols and r in f.rows
        if r < f.ruler:
            return RULE
        if ch in "+-|":
            return ACCENT_3 if lit else RULE
        return ACCENT if lit else TEXT

    glyphs(s, f.lines, color, at=(x0, y0), px=PX)
    if zoom:
        field = [row[f.cols.start : f.cols.stop] for row in f.lines[f.rows.start : f.rows.stop]]
        at_field = (x0 + f.cols.start * CW, y0 + detail * CH)
        glyphs(
            s,
            field,
            lambda c, r, ch: color(c + f.cols.start, r + f.rows.start, ch),
            at=at_field,
            px=PX,
        )
    if f.title:
        glyphs(s, f.title, DIM, at=(x0, y0 - 2 * CH), px=PX)
    if f.caption:
        glyphs(s, f.caption, DIM, at=(x0, y0 + (bottom - 1) * CH), px=PX)
    glyphs(s, f.note, lambda c, r, ch: ACCENT if c in f.lit else DIM, at=note, px=PX)

Run it yourself

$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render packet-header --theme fireproof -o packet-header-fireproof-16x9.svg