Skip to content

NTP Parsing Data Fields

Elbasiouny, Mahmoud edited this page May 29, 2026 · 5 revisions

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-32

Hammer concepts: h_sequence (continued) · splitting non-standard types


NTP Essential Fields Layout

 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.


Root Delay and Root Dispersion (32-bit Fixed-Point)

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 bits

The 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.


Reference Identifier (32-bit)

A plain unsigned 32-bit integer. Nothing special:

H_RULE(ref_id, h_uint32());  // 32 bits

Timestamps (64-bit)

NTP 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 bits

All 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.


Grouping Into essential_fields

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.

The Nesting Structure

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)

Complete Data Fields Code

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));

Summary

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

Clone this wiki locally