-
Notifications
You must be signed in to change notification settings - Fork 1
NTP Parsing Data Fields
After the 4-byte header, every NTP packet contains a fixed block of data fields: Root Delay, Root Dispersion, Reference Identifier, and four 64-bit timestamps. This page shows how to parse them and group everything into a single essential_fields rule.
Source file:
ntp.c, lines 22-32Hammer concepts:
h_sequence(continued) · splitting non-standard types
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Header (see previous page) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Delay |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Dispersion |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reference Identifier |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Reference Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Origin Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Receive Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Transmit Timestamp (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Total: 4 (header) + 4 + 4 + 4 + 8 + 8 + 8 + 8 = 48 bytes.
The NTP spec defines Root Delay and Root Dispersion as 32-bit signed fixed-point numbers (16 bits integer, 16 bits fraction). Hammer doesn't have a fixed-point parser, so we split each one into two 16-bit integers:
H_RULE(root_delay, h_sequence(h_int16(), h_int16(), NULL)); // 32 bits
H_RULE(root_disp, h_sequence(h_int16(), h_int16(), NULL)); // 32 bitsThe first h_int16() captures the integer portion, the second captures the fractional portion. A consumer of the parsed data can reconstruct the value:
fixed_point_value = integer_part + (fractional_part / 65536.0)
Why not just
h_uint32()? You could read 32 bits as a single integer, but splitting preserves the semantic meaning. The integer and fractional parts are separate values, which makes downstream processing easier.
A plain unsigned 32-bit integer. Nothing special:
H_RULE(ref_id, h_uint32()); // 32 bitsNTP timestamps are 64 bits: a 32-bit unsigned seconds count followed by a 32-bit unsigned fraction. Same splitting technique:
H_RULE(ref_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bits
H_RULE(org_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bits
H_RULE(rec_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bits
H_RULE(xmt_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bitsAll four timestamps follow the same pattern. You could extract this into a reusable rule:
H_RULE(ntp_timestamp, h_sequence(h_uint32(), h_uint32(), NULL));The current code keeps them separate for clarity, but both approaches work.
Just like we grouped the header sub-fields (previous page), we group all required fields into one rule:
H_RULE(essential_fields,
h_sequence(header, root_delay, root_disp, ref_id,
ref_ts, org_ts, rec_ts, xmt_ts, NULL));This single rule parses the entire 48-byte mandatory portion of an NTP packet. Notice how header (itself a sequence of 6 fields) nests cleanly inside the outer sequence. Combinators compose naturally.
essential_fields
├── header
│ ├── leap (2 bits)
│ ├── version (3 bits)
│ ├── mode (3 bits)
│ ├── stratum (8 bits)
│ ├── poll (8 bits)
│ └── precision (8 bits)
├── root_delay (16 + 16 bits)
├── root_disp (16 + 16 bits)
├── ref_id (32 bits)
├── ref_ts (32 + 32 bits)
├── org_ts (32 + 32 bits)
├── rec_ts (32 + 32 bits)
└── xmt_ts (32 + 32 bits)
From ntp.c:
// Essential fields
H_RULE(root_delay, h_sequence(h_int16(), h_int16(), NULL)); // 32 bits
H_RULE(root_disp, h_sequence(h_int16(), h_int16(), NULL)); // 32 bits
H_RULE(ref_id, h_uint32()); // 32 bits
H_RULE(ref_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bits
H_RULE(org_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bits
H_RULE(rec_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bits
H_RULE(xmt_ts, h_sequence(h_uint32(), h_uint32(), NULL)); // 64 bits
H_RULE(essential_fields,
h_sequence(header, root_delay, root_disp, ref_id,
ref_ts, org_ts, rec_ts, xmt_ts, NULL));| Situation | Approach |
|---|---|
| Fixed-point numbers | Split into two integer halves using h_sequence
|
| Large timestamps | Split into seconds + fraction, same technique |
| Many related fields | Group with h_sequence into a named rule |
| Repeated patterns | Consider extracting into a shared rule |
Next: Extension Fields and MAC - Variable-length optional fields.
Previous: Parsing the Header
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