Gonemaster Packet Cache File Format

This document specifies the on-disk format produced by gonemaster --save and consumed by gonemaster --restore. It covers:

  • the JSON schema,
  • the checksum contract,
  • how each cache kind is represented,
  • strict vs. lenient parsing behavior.

The format is implemented by engine/cachefile .

File layout

A single JSON object:

{
  "format": "gonemaster.packet-cache",
  "version": 2,
  "checksum": "<lowercase sha-256 hex>",
  "entries": [ ... ]
}
FieldTypeRequiredDescription
formatstringyesAlways "gonemaster.packet-cache". Foreign formats are rejected.
versionnumberyesSchema version. Currently 2. Unknown versions are rejected.
checksumstringno (*)Lowercase SHA-256 hex of the file with checksum blanked.
entriesarrayyesOrdered list of cache records, discriminated by kind.

(*) checksum is always written by --save and always verified when present. It is optional only so that hand-crafted fixtures can omit it; omission is a warning in lenient mode and an error in strict mode.

Checksum contract

The checksum covers the entire file with the checksum field itself set to the empty string, serialized using encoding/json.Marshal (no indentation, Go struct field order). It is lowercase hex SHA-256.

Verification rules:

  • Present and correct → file accepted.
  • Present and incorrectImport / Restore return an error (“checksum mismatch”), regardless of mode.
  • Absent → warning in lenient mode; error in strict mode.

The pretty-printed form produced by Save (indented JSON with a trailing newline) includes checksum, but the checksum value itself is computed against the canonical, non-indented, checksum-stripped form. Consumers that re-serialize an already-validated file must recompute the checksum if they want to preserve roundtripping.

Entries

Every entry carries a kind discriminator that selects the remaining fields:

  • "nameserver" - one cached response for a specific nameserver IP.
  • "recursor" - one cached response for the internal recursor.
  • "asn" - one cached ASN lookup result for a queried IP.

Unknown kind values are warnings in lenient mode (the entry is skipped) and errors in strict mode.

kind: "nameserver"

{
  "kind": "nameserver",
  "address": "192.0.2.53",
  "key": "example.com./A/IN",
  "answer_from": "192.0.2.53:53",
  "message": "<base64-encoded DNS wire packet>"
}

A cached empty (nil) response is encoded with "no_message": true and no message:

{
  "kind": "nameserver",
  "address": "192.0.2.53",
  "key": "example.com./A/IN",
  "no_message": true
}
FieldTypeRequiredDescription
addressstringyesNameserver IP (IPv4 or IPv6). Parsed with netip.
keystringyesInternal cache key; kept opaque to the file format.
messagestringrequired unless no_messageBase64 (std) of the wire-format DNS response packet.
answer_fromstringnoObserved source address of the response.
no_messagebooleannotrue for a cached empty/nil response.

kind: "recursor"

{
  "kind": "recursor",
  "name": "example.com",
  "qtype": "A",
  "qclass": "IN",
  "nameservers": [
    { "name": "ns1.example", "address": "192.0.2.1" },
    { "name": "ns2.example", "address": "2001:db8::1" }
  ],
  "message": "<base64-encoded DNS wire packet>"
}

An empty nameservers array (or omission) denotes a lookup that started from the root servers.

FieldTypeRequiredDescription
namestringyesQuery name (normalized, trailing dot stripped).
qtypestringyesDNS record type (uppercase, e.g., A, NS, DS).
qclassstringnoDNS class (defaults to IN).
nameserversarray of {name,address}noNameserver set consulted. Empty ⇒ started from root.
messagestringyesBase64 (std) of the wire-format DNS response packet.

The recursor cache only stores non-empty responses; there is no no_message form for recursor entries.

kind: "asn"

{
  "kind": "asn",
  "ip": "192.0.2.10",
  "asns": [64496, 64497],
  "prefix": "192.0.2.0/24",
  "raw": "64496 64497 | 192.0.2.0/24 | US | arin | 2001-01-01",
  "code": "AS_FOUND"
}
FieldTypeRequiredDescription
ipstringyesQueried IP address (IPv4 or IPv6). Parsed with netip.
asnsarraynoList of AS numbers. Empty for EMPTY_ASN_SET / ERROR_ASN_DATABASE.
prefixstringnoMost-specific routed prefix (e.g., 192.0.2.0/24). Parsed with netip.
rawstringnoBackend response line, captured verbatim for debugging.
codestringyesOne of AS_FOUND, EMPTY_ASN_SET, ERROR_ASN_DATABASE.

ASN entries are stored on hit and on miss (EMPTY_ASN_SET, ERROR_ASN_DATABASE), so a restored cache avoids both successful and failed lookups.

Strict vs. lenient parsing

Import and Restore accept functional options:

  • WithStrict() - turn every non-fatal warning into an error.
  • WithWarnf(fn) - route non-fatal warnings to a callback.

In lenient mode (default) the following produce warnings but keep processing:

  • Missing checksum.
  • Unknown top-level field (e.g., a future stats section).
  • Unknown field inside an entry.
  • Entry with an empty or unknown kind (the entry is skipped).

In strict mode the same conditions are hard errors.

Any base64 decode failure, DNS unpack failure, invalid IP address, version mismatch, or checksum mismatch is always a hard error regardless of mode.

Minimal example

{
  "format": "gonemaster.packet-cache",
  "version": 2,
  "checksum": "0000000000000000000000000000000000000000000000000000000000000000",
  "entries": [
    {
      "kind": "nameserver",
      "address": "192.0.2.53",
      "key": "example.com./A/IN",
      "answer_from": "192.0.2.53:53",
      "message": "PQ4BAAABAAEAAAAAB2V4YW1wbGUDY29tAAABAAE="
    },
    {
      "kind": "recursor",
      "name": "example.net",
      "qtype": "NS",
      "qclass": "IN",
      "nameservers": [
        { "name": "ns1.example", "address": "192.0.2.1" }
      ],
      "message": "E+6BAAABAAAAAAAAB2V4YW1wbGUDbmV0AAACAAE="
    },
    {
      "kind": "asn",
      "ip": "192.0.2.53",
      "asns": [64496],
      "prefix": "192.0.2.0/24",
      "code": "AS_FOUND"
    }
  ]
}

Replace the placeholder checksum with the real SHA-256 when writing a file by hand, or let gonemaster --save produce it.