-
Notifications
You must be signed in to change notification settings - Fork 1
NTP Extension Fields and MAC
This is the most advanced section of the NTP example. NTP packets can optionally include extension fields (variable-length, zero or more) and a MAC (message authentication code). Parsing these requires new combinators: h_put_value, h_free_value, h_action, h_length_value, and h_many.
Source file:
ntp.c, lines 34-47Hammer concepts:
h_put_value·h_free_value·h_action·h_length_value·h_many
After the 48 mandatory bytes (essential fields), an NTP packet may contain zero or more extension fields. Each has this structure:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Field Type | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
. .
. Value (variable) .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The Length field gives the total size of the extension field including the 4-byte header (Field Type + Length). So the actual value data is Length - 4 bytes.
This "length includes its own header" pattern is extremely common in binary protocols.
We need to:
- Parse the Field Type (16 bits) - easy.
- Parse the Length (16 bits) - easy.
- Use that Length value to know how many bytes of Value data to read - hard.
Step 3 is hard because the length of the Value depends on a previously parsed value. Standard combinators like h_sequence can't express this since each sub-parser is independent. We need data-dependent parsing.
Nothing new here:
H_RULE(field_type, h_int16()); // 16 bitsWe need to parse the 16-bit length and save its value for later use. h_put_value does both.
| Parameter | Type | Description |
|---|---|---|
parser |
HParser * |
The parser to run |
name |
const char * |
A string key to store the result under |
This parses normally and stores the result in a named "slot" that other combinators can retrieve later.
h_put_value(h_uint16(), "opt_len_val")This reads a 16-bit unsigned integer and stores it under "opt_len_val".
To get the stored value back:
| Parameter | Type | Description |
|---|---|---|
name |
const char * |
The key that was passed to h_put_value
|
This creates a parser that produces the previously stored value and removes it from the store. The "free" means the binding is cleaned up after use. This is important when parsing multiple extension fields, since each one reuses the same name "opt_len_val".
h_free_value("opt_len_val") // Produces the stored uint16 valueThe stored length includes the 4-byte header (Field Type + Length), but we only want to read Length - 4 bytes of value data. We need to subtract 4.
| Parameter | Type | Description |
|---|---|---|
parser |
HParser * |
The parser whose result will be transformed |
action_fn |
HParsedToken *(*)(const HParseResult *, void *) |
A C function that transforms the parse result |
user_data |
void * |
Optional data passed to action_fn (usually NULL) |
The action function receives the parse result and returns a new HParsedToken. From ntp.c:
HParsedToken *opt_ext_len(const HParseResult *p, void *user_data) {
unsigned int temp = p->ast->uint;
return H_MAKE_UINT(temp - 4);
}It reads the integer value, subtracts 4, and returns a new unsigned integer token.
Now we wrap retrieval and transformation together:
h_action(h_free_value("opt_len_val"), opt_ext_len, NULL)This produces a parser that yields the adjusted length.
Finally, we use the adjusted length to read exactly that many bytes.
| Parameter | Type | Description |
|---|---|---|
length_parser |
HParser * |
A parser that produces an integer n |
value_parser |
HParser * |
A parser to run n times |
h_length_value runs length_parser to get a count, then runs value_parser that many times.
h_length_value(
h_action(h_free_value("opt_len_val"), opt_ext_len, NULL), // -> n
h_uint8() // run n times
)If the Length field was 0x0008 (8), then opt_ext_len computes 8 - 4 = 4, and h_length_value reads 4 bytes (four h_uint8() calls).
Putting it all together:
H_RULE(ext_field,
h_sequence(
field_type,
h_put_value(h_uint16(), "opt_len_val"),
h_length_value(
h_action(h_free_value("opt_len_val"), opt_ext_len, NULL),
h_uint8()),
NULL));The data flow:
Input bytes: [Field Type: 2B] [Length: 2B] [Value: (Length-4) bytes]
│ │ │
field_type h_put_value ──store──► │
│ │
h_free_value ◄──retrieve── │
│ │
h_action (subtract 4) │
│ │
h_length_value ────────► read N bytes
An NTP packet can have zero or more extension fields. h_many handles this:
Matches parser zero or more times, collecting results into a sequence. It's greedy: it keeps matching until parser fails, then succeeds with however many matches it found.
H_RULE(ext_fields, h_many(ext_field));If there are no extension fields, ext_fields succeeds with an empty sequence. If there are three, it produces a sequence of three parsed extension fields.
Why
h_free_valuematters: Each extension field stores its length under"opt_len_val".h_free_valuecleans up the binding after each use, so the next extension field can reuse the same name. Without it, the second extension field would see a stale value.
After the extension fields, the packet may end with an optional MAC:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Key Identifier |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Message Digest (128 bits) |
| |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The MAC is a fixed-size structure:
H_RULE(key_id, h_uint32()); // 32 bits
H_RULE(dgst, h_sequence(h_int64(), h_int64(), NULL)); // 128 bits
H_RULE(mac, h_sequence(key_id, dgst, NULL));The 128-bit message digest is read as two 64-bit integers (same splitting technique from timestamps).
| Combinator | What It Does | Why We Need It |
|---|---|---|
h_put_value(p, name) |
Parse with p, store result as name
|
Save the length field for later |
h_free_value(name) |
Retrieve and remove a stored value | Get the length back (and clean up for reuse) |
h_action(p, fn, data) |
Parse with p, transform result with fn
|
Subtract 4 from the length |
h_length_value(len, p) |
Use len's result as a repeat count for p
|
Read exactly Length - 4 bytes |
h_many(p) |
Match p zero or more times |
Handle variable number of extension fields |
Next: Assembling the Parser - Combine everything with h_choice and h_left.
Previous: Parsing Data Fields
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