Reading Network Packets as Hex: A Practical Debugging Workflow
Learn how hex encoding maps binary data to readable strings, dissect real HTTP and DNS packet bytes, and build a workflow for diagnosing protocol bugs without Wireshark GUI.
Reading Network Packets as Hex: A Practical Debugging Workflow
When a network connection misbehaves at the protocol level, the fastest way to know what is actually on the wire is to look at the raw bytes. Not the parser's interpretation, not a pretty GUI table — the bytes themselves, written as two hex digits each.
I learned this the hard way debugging a custom binary framing protocol where the server and client disagreed about message length. Both sides looked correct in source code. The bug was hiding in exactly one byte: a length field encoded as little-endian that one side was reading as big-endian. Opening a hex dump of the capture ended a three-hour argument in about four minutes.
This guide covers what hex encoding is at the byte level, how to read protocol fields by byte position, and a concrete workflow for diagnosing network bugs without reaching for a Wireshark GUI.
Why Hex Encoding and Not Binary or Decimal
A byte holds a value from 0 to 255. Written in decimal, that is up to three characters per byte — inconsistent width. Written in raw binary, that is eight characters per byte — too wide to scan. Hexadecimal uses exactly two characters per byte at all times: values 0–15 use a leading zero (00 through 0f), values 16–255 use two digits (10 through ff).
This fixed two-character width is the reason hex survived. A 20-byte row of protocol data prints in exactly 40 hex characters, or 48 with spaces between pairs. You can count offsets with mental arithmetic: offset 0x10 is byte 16, offset 0x40 is byte 64, offset 0x100 is byte 256. That regularity lets you find any field in a capture by offset without counting individual characters.
The four-bits-per-digit rule is also useful for bit-field inspection. A TLS record type occupies the entire first byte. If that byte reads 16 in hex, the binary is 0001 0110 — the high nibble 1 and low nibble 6. Version bytes and flags often pack multiple fields into one byte, and reading the hex directly lets you split them by nibble without binary-to-decimal conversion.
Dissecting a Real HTTP/1.1 Request Byte by Byte
Take a minimal HTTP GET request sent to a server: GET / HTTP/1.1\r\nHost: example.com\r\n\r\n
Running that string through a UTF-8 byte converter and viewing it as a hex dump gives:
00000000 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a GET / HTTP/1.1..
00000010 48 6f 73 74 3a 20 65 78 61 6d 70 6c 65 2e 63 6f Host: example.co
00000020 6d 0d 0a 0d 0a m....
The double 0d 0a 0d 0a at offset 0x23 is the blank line that separates headers from body. If you are writing a parser and it is not finding the header boundary, the first thing to verify is that both CRLFs are present as 0d 0a — not just 0a (Unix line endings) and not 0d alone (old Mac line endings). Parsers that accept only one form will silently fail with the other.
The ASCII gutter on the right makes this visible without reading every byte: you see .. at the end of the first row (two non-printable bytes — the first CRLF) and .... at the end of the last row (the blank line).
The Hex Dump Viewer renders this layout for any text input, using the same column format as xxd and hexdump -C. Paste the request string directly and it shows you the offset, bytes, and ASCII preview without installing any tools.
Reading a DNS Query at the Byte Level
DNS queries have a well-specified 12-byte header followed by a variable-length question section. Here is an actual DNS A-record query for www.example.com, captured from a real resolver:
Offset Hex bytes
000000 a3 5f 01 00 00 01 00 00 00 00 00 00
00000c 03 77 77 77 07 65 78 61 6d 70 6c 65
000018 03 63 6f 6d 00 00 01 00 01
Breaking down the fixed 12-byte header (RFC 1035 §4.1.1):
| Offset | Bytes | Meaning | |---|---|---| | 0–1 | a3 5f | Transaction ID — random value, matched in the response | | 2–3 | 01 00 | Flags — 01 00 = standard query, recursion desired | | 4–5 | 00 01 | QDCOUNT — 1 question | | 6–7 | 00 00 | ANCOUNT — 0 answers (this is a query, not a response) | | 8–9 | 00 00 | NSCOUNT — 0 | | 10–11 | 00 00 | ARCOUNT — 0 |
The question section starts at offset 12 (0x0c). DNS names use length-prefixed labels:
03→ 3-byte label follows77 77 77→w w w07→ 7-byte label follows65 78 61 6d 70 6c 65→example03→ 3-byte label63 6f 6d→com00→ root label, terminates the name
Following the name: 00 01 = QTYPE A (IPv4 address), 00 01 = QCLASS IN (internet). The full question is exactly 21 bytes.
If a resolver returns SERVFAIL and your logs show a correctly formatted question but the response is refusing it, checking the flags byte (01 00 versus 81 00) can reveal whether the response is actually a query answer or a retransmission of your own packet — a subtle bug in some UDP NAT implementations.
A Practical Hex Debugging Workflow
When a protocol bug is not visible in logs, this sequence usually isolates it:
Step 1 — Capture raw bytes. Use tcpdump -w capture.pcap on Linux or the built-in packet capture on macOS. If you cannot run a packet capture, add temporary logging that hexlogs the bytes your application sends and receives.
Step 2 — Convert the relevant segment to hex. xxd capture.pcap | head -20 for a quick look, or extract a specific packet payload with tcpdump -r capture.pcap -X -nn. For application-level bytes that are already in memory as a string, paste them into the Text to Hex Converter to get the exact byte sequence in your preferred separator format.
Step 3 — Locate the field by offset. Count bytes from the start of the payload to the field you care about. For HTTP, the request line ends at the first 0d 0a; for DNS, field offsets are fixed by the RFC and listed in the table above. Write the offsets down — debugging sessions that skip this step tend to lose track of which byte is which.
Step 4 — Compare against the spec. Is the field the right width? Is a multi-byte integer in the right byte order? Are bitfields set correctly? The answer to at least one of these is "no" in the majority of protocol bugs I have seen.
Step 5 — Confirm the fix. After changing the code, capture again and verify the byte sequence at the same offsets. Do not assume the fix worked — check the hex.
A single Ethernet frame adds a 14-byte header (6 bytes destination MAC + 6 bytes source MAC + 2 bytes EtherType), which means the IP payload starts at offset 14 (0x0e) in a raw capture. TCP adds another 20 bytes minimum (offset 0x22 for a standard TCP payload start). Knowing these constants cuts the time spent scanning a raw capture roughly in half, because you can jump directly to the application layer data.
When Base64 Sits Between You and the Bytes
Some protocols, like SMTP with attachments or HTTP Basic Auth headers, encode binary data as Base64 before transmission. The string SGVsbG8sIFdvcmxkIQ== looks like noise but decodes to Hello, World!. Getting to the raw bytes means decoding the Base64 first — and then inspecting the result as hex if the decoded content is itself binary.
Tools like Base64 to Hex Converter handle this in one step: paste the Base64 and get the hex bytes directly, with no intermediate text-encoding step that could silently corrupt non-ASCII byte values.
Made by Toolora · Updated 2026-06-27