-
Notifications
You must be signed in to change notification settings - Fork 1
NTP Parsing the Header
The NTP header is the first 32 bits of every NTP packet. It packs six fields into a single 4-byte row, three of which are sub-byte (fewer than 8 bits). This makes it a good place to learn h_bits, h_int_range, and h_sequence.
Source file:
ntp.c, lines 11-20Hammer concepts:
h_bits·h_int_range·h_sequence
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN |Mode | Stratum | Poll | Precision |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2 3 3 8 8 8 (bits)
| Field | Bits | Type | Valid Range | Description |
|---|---|---|---|---|
| LI (Leap Indicator) | 2 | unsigned | 0-3 | Leap second warning |
| VN (Version Number) | 3 | unsigned | 1-4 | NTP protocol version |
| Mode | 3 | unsigned | 0-7 | Operating mode (client, server, etc.) |
| Stratum | 8 | unsigned | 0-16 | Stratum level |
| Poll | 8 | signed | any | Log2 of poll interval in seconds |
| Precision | 8 | signed | any | Log2 of system clock precision |
The first byte contains three fields that together occupy 8 bits: LI (2), VN (3), and Mode (3). Standard parsers like h_uint8() read a minimum of 8 bits, so we need something finer-grained.
| Parameter | Type | Description |
|---|---|---|
len |
size_t |
Number of bits to read |
signed |
bool |
Whether to interpret the bits as a signed value |
Hammer reads bits MSB-first (most significant bit first), so reading 2 bits followed by 3 bits followed by 3 bits correctly splits the first byte.
h_bits returns a 64-bit integer token; it is best suited for sub-byte and other
integer fields up to 64 bits. For wider fields, use h_bytes() or split the
field into smaller integer parsers.
h_bits(2, false) // Read 2 bits -> values 0-3
h_bits(3, false) // Read 3 bits -> values 0-7Not every bit pattern is valid. The NTP spec says Version Number must be 1-4 and Mode must be 0-7. h_int_range wraps a parser and rejects values outside a given range.
| Parameter | Type | Description |
|---|---|---|
parser |
HParser * |
The inner parser whose value is checked |
lower |
int64_t |
Minimum acceptable value (inclusive) |
upper |
int64_t |
Maximum acceptable value (inclusive) |
If the parsed value falls outside [lower, upper], the parse fails, just as if the input didn't match at all. This lets you express validity rules within the grammar rather than checking values after parsing.
h_int_range(h_bits(3, false), 1, 4) // 3 bits, must be 1-4Now we define each header field as an H_RULE. From ntp.c:
// Header
H_RULE(leap, h_int_range(h_bits(2, false), 0, 3)); // 2 bits, must be between 0-3
H_RULE(version, h_int_range(h_bits(3, false), 1, 4)); // 3 bits, must be between 1-4
H_RULE(mode, h_int_range(h_bits(3, false), 0, 7)); // 3 bits, must be between 0-7
H_RULE(stratum, h_int_range(h_uint8(), 0, 16)); // 8 bits, must be between 0-16
H_RULE(poll, h_int8()); // 8 bits
H_RULE(precision, h_int8()); // 8 bitsThe pattern:
-
Sub-byte fields use
h_bits(n, false)wrapped inh_int_range. -
Full-byte fields with constraints use
h_uint8()wrapped inh_int_range. -
Full-byte fields without constraints use
h_int8()directly.
Tip: Always validate at the grammar level when the spec defines valid ranges. Invalid packets get rejected during parsing, so you don't need separate validation logic.
Six individual field parsers aren't useful on their own. We need them to run in order on consecutive bits.
Parses p1, then p2, then ... in order. The result is a sequence (array) of all parsed values. The parameter list must end with NULL.
H_RULE(header, h_sequence(leap, version, mode, stratum, poll, precision, NULL));When header runs against input, it:
- Reads 2 bits ->
leap - Reads 3 bits ->
version - Reads 3 bits ->
mode - Reads 8 bits ->
stratum - Reads 8 bits ->
poll - Reads 8 bits ->
precision
If any step fails (e.g., version is 7, which is outside 1-4), the entire header parse fails. Failure propagates automatically.
Putting it all together, from ntp.c:
H_RULE(leap, h_int_range(h_bits(2, false), 0, 3));
H_RULE(version, h_int_range(h_bits(3, false), 1, 4));
H_RULE(mode, h_int_range(h_bits(3, false), 0, 7));
H_RULE(stratum, h_int_range(h_uint8(), 0, 16));
H_RULE(poll, h_int8());
H_RULE(precision, h_int8());
H_RULE(header, h_sequence(leap, version, mode, stratum, poll, precision, NULL));Given the hex byte 0xE3 as the first byte of input:
Binary: 1 1 1 0 0 0 1 1
└─┘ └──┘ └──┘
LI VN Mode
3 4 3
-
leapreads 2 bits ->3(valid: 0-3) -
versionreads 3 bits ->4(valid: 1-4) -
modereads 3 bits ->3(valid: 0-7)
The header parser then continues reading stratum, poll, and precision from the next 3 bytes.
| Concept | Hammer Function | When to Use |
|---|---|---|
| Parse fewer than 8 bits | h_bits(n, signed) |
Protocol fields smaller than a byte |
| Constrain parsed values | h_int_range(p, lo, hi) |
Spec defines a valid range |
| Parse fields in order | h_sequence(a, b, ..., NULL) |
Multiple fields appear consecutively |
| Name a parser | H_RULE(name, def) |
Always, it makes the grammar readable |
Next: Parsing Data Fields
Previous: NTP Overview
Learn Hammer
Protocol Examples
NTP
- NTP Overview
- Parsing the Header
- Parsing Data Fields
- Extension Fields and MAC
- Assembling the Parser
- Hex Input Preprocessing
- Running and Testing
DNS
TFTP
- TFTP Overview
- RRQ/WRQ Packets
- DATA Packets
- ACK Packets
- ERROR Packets
- Assembling the Parser
- Running and Testing
References
- Hammer Quick Reference
- Parsing Backends
- Unit Testing
- Using RTEMS
- Extending Hammer
- Adding a New Example
- Adding a New Binding
Further Reading