From 3bf35d26953de8d60e7ecd402885ac84d07b5da8 Mon Sep 17 00:00:00 2001 From: Jorge Leal Date: Tue, 19 May 2026 12:22:13 +0200 Subject: [PATCH 1/2] feat: add X-PE3-FIXED-SERIAL metadata to pin the SOA serial When set on a zone, this metadata key replaces the automatic zoneRev-derived serial in the SOA record (parsed as uint32, per RFC 1035). Invalid/missing values fall back to zoneRev() unchanged. Motivation: pre-signed DNSSEC needs the served SOA serial to match the one baked into RRSIG(SOA); otherwise validators reject the answer. Also useful without DNSSEC for manual serial control. Pre-signed zones otherwise need no new code: PRESIGNED=1 rides the existing metadata mechanism, and DNSSEC record types (DNSKEY, RRSIG, NSEC*, DS, CDS, CDNSKEY) fall through the default plain-string path and are served verbatim. --- README.md | 9 +++- doc/ETCD-structure.md | 32 ++++++++++++ src/const.go | 1 + src/dnssec_test.go | 112 ++++++++++++++++++++++++++++++++++++++++++ src/rr.go | 20 +++++++- 5 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/dnssec_test.go diff --git a/README.md b/README.md index c25d9e7..c04c483 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ the fourth development release, considered alpha quality. Any testing is appreci * [Multi-level defaults and options](doc/ETCD-structure.md#defaults-and-options), overridable * [Domain metadata](https://doc.powerdns.com/authoritative/domainmetadata.html) * can also be read and modified with the command line tool `pdnsutil` (`pdnssec` in v3.4) +* [Pre-signed DNSSEC zones](doc/ETCD-structure.md#pre-signed-dnssec) (online signing is still [planned](#planned)) + * an external signer (e.g. `ldns-signzone`, `dnssec-signzone`, OpenDNSSEC) produces the signed records (`RRSIG`, `NSEC`/`NSEC3`, `DNSKEY`, …) and stores them in ETCD just like any other record + * `PRESIGNED=1` is set as ordinary [metadata](doc/ETCD-structure.md#metadata) — there is no DNSSEC-specific switch on the backend + * the served SOA serial can be pinned with the [`X-PE3-FIXED-SERIAL`](doc/ETCD-structure.md#metadata) metadata to match the value baked into `RRSIG(SOA)` by the signer * [Upgrade data structure](doc/ETCD-structure.md#upgrading) (if needed for new program version) without interrupting service * Run [standalone](#standalone-modes) for usage as a [Unix or HTTP connector][pdns-remote-usage] * This could be needed for big data sets, because the initialization from PowerDNS is done lazily (at least as of v4) on first request (which possibly could time out on "big data"…) :-( @@ -59,7 +63,8 @@ the fourth development release, considered alpha quality. Any testing is appreci * "Labels" for selectively applying defaults and/or options to record entries * sth. like `com/example/-options-ptr` → `{"auto-ptr": true}` and `com/example/www/-options-collect` → `{"collect": …}` for `com/example/www-1/A+ptr+collect` without global options * precedence betweeen QTYPE and id (id > label > QTYPE) -* DNSSEC support ([PowerDNS DNSSEC-specific calls][pdns-dnssec]) +* Online DNSSEC signing ([PowerDNS DNSSEC-specific calls][pdns-dnssec]) + * pre-signed zones are already supported, see [Features](#features) and [ETCD structure](doc/ETCD-structure.md#pre-signed-dnssec) * [Search][pdns-search] support [pdns-dnssec]: https://doc.powerdns.com/authoritative/appendices/backend-writers-guide.html#dnssec-support @@ -83,7 +88,7 @@ the fourth development release, considered alpha quality. Any testing is appreci ### Overview over the support of optional [PDNS features in a remote backend][pdns-remote]: * Primary and (Auto)Secondary: no * AXFR support: not yet -* DNSSEC (live-signing): not yet (planned feature) +* DNSSEC: pre-signed yes, live-signing not yet (planned feature) * Metadata: yes * Search (web API): not yet (planned feature) * API lookup (for web): not yet diff --git a/doc/ETCD-structure.md b/doc/ETCD-structure.md index fcccaaa..abfc905 100644 --- a/doc/ETCD-structure.md +++ b/doc/ETCD-structure.md @@ -297,11 +297,41 @@ this is especially important when using user-defined keys, beginning with `X-`. The values of metadata entries are always read as strings and not modified in any way, just passed as-is to PDNS. +### Reserved `X-PE3-*` keys + +Metadata keys starting with `X-PE3-` are reserved for use by this backend. They are not forwarded to PowerDNS as ordinary metadata (PDNS already ignores unknown `X-` keys), but they alter how pdns-etcd3 serves the zone: + +* `X-PE3-FIXED-SERIAL` — when set on a zone, its first value (parsed as an unsigned 32-bit integer per [RFC 1035][rfc1035]) replaces the automatic [zone-revision serial](#soa) in the served `SOA` record. Anything that does not parse (out of range, unparseable, empty) is logged and the automatic serial is used instead. + * The main use case is [pre-signed DNSSEC](#pre-signed-dnssec): the served `SOA` serial must match the value the external signer baked into `RRSIG(SOA)`, otherwise validating resolvers reject the answer. + * It is also useful without DNSSEC, for operators who want to control the serial manually (e.g. to mirror an existing zone byte-for-byte). +* `X-PE3-MINIMUM-SERIAL` — internal book-keeping. It is added/removed automatically by `setDomainMetadata` transactions to ensure the zone revision (and thus the serial) never moves backwards across a key-delete. Operators normally do not write this key by hand. + +[rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 + ## Locks Every zone can be locked for transactions. These entries have a prefix of `/-lock-` and are handled automatically, there is no need to create or delete them manually. They are not part of the automatic SOA serial determination. +## Pre-signed DNSSEC + +pdns-etcd3 supports DNSSEC in the [pre-signed (front-signing)][rfc4035-presigned] model: an external signer (`ldns-signzone`, `dnssec-signzone`, OpenDNSSEC, …) produces the signed records and pushes them into ETCD under the same key layout as ordinary records. The backend itself does not sign anything; [online signing](https://doc.powerdns.com/authoritative/dnssec/modes-of-operation.html) is on the [Planned](../README.md#planned) list. + +Setup checklist: + +1. Store each signed record as a plain presentation-format string under its name and qtype. The relevant qtypes (`DNSKEY`, `RRSIG`, `NSEC`, `NSEC3`, `NSEC3PARAM`, `DS`, `CDS`, `CDNSKEY`) are not object-supported and have no plain-string parser — they are passed through to PowerDNS unchanged, which is exactly what pre-signed zones need. + * As always, [start such values with a backtick](#resource-record-values) if the first character is non-alphanumeric — for example `RRSIG` entries that begin with whitespace, parentheses or a leading sign character. +2. Set `PRESIGNED=1` as ordinary metadata on the zone, e.g. via `pdnsutil set-meta PRESIGNED 1` or by writing `/-metadata-/PRESIGNED` directly. PowerDNS will then serve `RRSIG`/`NSEC`/`DNSKEY` records from the backend instead of trying to sign on the fly. +3. (Recommended) Set [`X-PE3-FIXED-SERIAL`](#reserved-x-pe3--keys) on the zone to the serial value the signer wrote into `SOA` / `RRSIG(SOA)`. Update it in the same transaction as the new signed RRsets so the served serial stays consistent with the signatures. + +Operational notes: + +* Re-signing happens outside the backend. When a re-sign batch lands in ETCD, the zone is reloaded as usual. +* `NSEC`/`NSEC3` chains must remain consistent at all times. Push chain updates atomically (e.g. using a single ETCD transaction or [`setDomainMetadata`-style locks](#locks)) so resolvers never observe a broken denial-of-existence chain. +* `getDomainKeys` is not implemented — pdns-etcd3 does not store DNSSEC private keys. This is fine for `PRESIGNED=1` zones; it is the missing piece for online signing. + +[rfc4035-presigned]: https://datatracker.ietf.org/doc/html/rfc4035 + ## Supported records For each of the supported record types the entry values may be objects. @@ -459,6 +489,8 @@ as described in syntax for "domain name"! It also can be only the local part (wi There is no serial field, because the program takes the latest modification revision of the zone as serial. This way the operator does not have to increase it manually each time he/she changes DNS data. +If the automatic serial does not fit the use case (e.g. [pre-signed DNSSEC zones](#pre-signed-dnssec) where the serial must match the value the signer put into `RRSIG(SOA)`, or operators wanting full manual control), set the [`X-PE3-FIXED-SERIAL`](#reserved-x-pe3--keys) metadata on the zone. Its value (parsed as an unsigned 32-bit integer per RFC 1035) is then served verbatim in the `SOA` record. With no such metadata, the automatic serial is used. + Options: * `no-aa` or `not-authoritative`: boolean * don't set the AA-bit for this zone, when set to true diff --git a/src/const.go b/src/const.go index 2a41c48..b387d24 100644 --- a/src/const.go +++ b/src/const.go @@ -63,6 +63,7 @@ const ( const ( MetaMinimumSerial = "X-PE3-MINIMUM-SERIAL" + MetaFixedSerial = "X-PE3-FIXED-SERIAL" ) type ipMetaT map[int]struct { diff --git a/src/dnssec_test.go b/src/dnssec_test.go new file mode 100644 index 0000000..ea87ede --- /dev/null +++ b/src/dnssec_test.go @@ -0,0 +1,112 @@ +//go:build unit + +/* Copyright 2016-2026 nix + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +package src + +import ( + "fmt" + "testing" +) + +// TestFixedSerial: MetaFixedSerial metadata overrides zoneRev() in the SOA serial. +func TestFixedSerial(t *testing.T) { + type meta = []string + for i, spec := range []test[meta, int64]{ + // no metadata at all → zoneRev fallback (maxRev=42 set below) + {nil, ve[int64]{v: 42}}, + // empty list → zoneRev fallback + {meta{}, ve[int64]{v: 42}}, + // valid override + {meta{"123456"}, ve[int64]{v: 123456}}, + // whitespace is trimmed + {meta{" 2026010101 "}, ve[int64]{v: 2026010101}}, + // uint32 boundary still accepted + {meta{"4294967295"}, ve[int64]{v: 4294967295}}, + // out of uint32 range → zoneRev fallback + {meta{"4294967296"}, ve[int64]{v: 42}}, + // negative / unparseable → zoneRev fallback + {meta{"-1"}, ve[int64]{v: 42}}, + {meta{"abc"}, ve[int64]{v: 42}}, + // extra entries are ignored — first one wins + {meta{"7", "8", "9"}, ve[int64]{v: 7}}, + } { + tf := func(_ *testing.T, in meta) (int64, error) { + dn := newDataNode(nil, "", "TEST/", false) + dn.maxRev = 42 + if in != nil { + dn.metadata[MetaFixedSerial] = in + } + params := &rrParams{qtype: "SOA", id: "", data: dn} + return params.fixedSerial(), nil + } + checkRun(t, fmt.Sprintf("(%d)%v", i+1, spec.input), tf, spec.input, spec.expected, false) + } +} + +// TestSOAFixedSerialThroughProcessValues: the override actually lands in the served SOA content. +func TestSOAFixedSerialThroughProcessValues(t *testing.T) { + RootLog.ChildLog("data").SetLevel(10) + root := newDataNode(nil, "", "TEST/", false) + zone := root.getChildCreate([]namePart{{"tld", ""}}) + zone.metadata[MetaFixedSerial] = []string{"2026051901"} + soaValues := objectValueType{ + "primary": "ns", + "mail": "horst.master", + "refresh": "1h", + "retry": "30m", + "expire": 604800, + "neg-ttl": "10m", + } + zone.processValuesEntry("SOA", "", &valueType{key: "SOA", content: soaValues}) + got, ok := zone.records["SOA"][""] + if !ok { + t.Fatalf("expected an SOA record, got none") + } + want := `ns.tld. horst\.master.tld. 2026051901 3600 1800 604800 600` + if got.content != want { + t.Errorf("SOA content mismatch:\n got: %q\nwant: %q", got.content, want) + } +} + +// TestDNSSECPlainStringPassthrough: DNSSEC qtypes (no parser, no rrFunc) round-trip verbatim. +func TestDNSSECPlainStringPassthrough(t *testing.T) { + RootLog.ChildLog("data").SetLevel(10) + root := newDataNode(nil, "", "TEST/", false) + zone := root.getChildCreate([]namePart{{"tld", ""}}) + cases := map[string]string{ + "DNSKEY": "257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==", + "RRSIG": "A 13 2 3600 20260601000000 20260501000000 12345 example.com. abc123==", + "NSEC": "host.example.com. A NS SOA MX AAAA RRSIG NSEC DNSKEY", + "NSEC3": "1 0 10 ABCD H9P7U7TR2U91D0V0LJS9L1GIDNP90U3H A RRSIG", + "NSEC3PARAM": "1 0 10 ABCD", + "DS": "12345 13 2 3B1AAAAABBBBCCCC", + "CDS": "0 0 0 00", + "CDNSKEY": "0 3 0 AA==", + } + for qtype, content := range cases { + t.Run(qtype, func(t *testing.T) { + clearMap(zone.records) + zone.processValuesEntry(qtype, "", &valueType{key: qtype, content: stringValueType{s: content}}) + got, ok := zone.records[qtype][""] + if !ok { + t.Fatalf("expected a %s record, got none", qtype) + } + if got.content != content { + t.Errorf("%s content mismatch:\n got: %q\nwant: %q", qtype, got.content, content) + } + }) + } +} diff --git a/src/rr.go b/src/rr.go index 25c3c66..4f8aecc 100644 --- a/src/rr.go +++ b/src/rr.go @@ -267,6 +267,22 @@ func domainName(key string) rrFunc { } } +// fixedSerial returns MetaFixedSerial (if set and a valid uint32) or zoneRev() otherwise. +func (p *rrParams) fixedSerial() int64 { + values, ok := p.data.metadata[MetaFixedSerial] + if !ok || len(values) == 0 { + return p.data.zoneRev() + } + raw := strings.TrimSpace(values[0]) + v, err := strconv.ParseUint(raw, 10, 32) + if err != nil { + p.Logf(WarningLevel)("invalid %s metadata, falling back to zoneRev", MetaFixedSerial)("value", raw, "err", err) + return p.data.zoneRev() + } + p.Logf(3)("using %s metadata as SOA serial", MetaFixedSerial)("serial", v) + return int64(v) +} + func soa(params *rrParams) { // primary primary, vPath, err := getValue[string]("primary", params) @@ -303,8 +319,8 @@ func soa(params *rrParams) { params.Logf(ErrorLevel)("failed to append zone domain to 'mail': %v", err)("vp", Supplier1(ptr2strS, vPath)) return } - // serial - serial := params.data.zoneRev() // no need for findZone(), because SOA defines the zone + // serial: MetaFixedSerial overrides zoneRev (e.g. to match RRSIG(SOA) in pre-signed mode). + serial := params.fixedSerial() // refresh refresh, vPath, err := getDuration("refresh", params) if vPath == nil || err != nil { From 26e1a68f5af4eaf368d53ce83d9113177c13bd65 Mon Sep 17 00:00:00 2001 From: Jorge Leal Date: Mon, 8 Jun 2026 07:16:58 +0200 Subject: [PATCH 2/2] docs/refactor: address review feedback on pre-signed DNSSEC - move soaSerial out of the *rrParams receiver into a free function (SOA-specific logic shouldn't hang off the receiver common to all RRs) - drop the redundant plain-string passthrough test (verbatim serving of alphanumeric-leading plain strings is already covered by the default path) - docs: tighten the X-PE3-MINIMUM-SERIAL note, the pre-signed intro and the NSEC-chain wording; trim the README Features bullet --- README.md | 5 +++-- doc/ETCD-structure.md | 6 +++--- src/dnssec_test.go | 33 +-------------------------------- src/rr.go | 16 ++++++++-------- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index c04c483..8ceb25b 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,11 @@ the fourth development release, considered alpha quality. Any testing is appreci * [Multi-level defaults and options](doc/ETCD-structure.md#defaults-and-options), overridable * [Domain metadata](https://doc.powerdns.com/authoritative/domainmetadata.html) * can also be read and modified with the command line tool `pdnsutil` (`pdnssec` in v3.4) -* [Pre-signed DNSSEC zones](doc/ETCD-structure.md#pre-signed-dnssec) (online signing is still [planned](#planned)) +* [Pre-signed DNSSEC zones](doc/ETCD-structure.md#pre-signed-dnssec) * an external signer (e.g. `ldns-signzone`, `dnssec-signzone`, OpenDNSSEC) produces the signed records (`RRSIG`, `NSEC`/`NSEC3`, `DNSKEY`, …) and stores them in ETCD just like any other record - * `PRESIGNED=1` is set as ordinary [metadata](doc/ETCD-structure.md#metadata) — there is no DNSSEC-specific switch on the backend + * `PRESIGNED=1` is set as [metadata](doc/ETCD-structure.md#metadata) on the zone * the served SOA serial can be pinned with the [`X-PE3-FIXED-SERIAL`](doc/ETCD-structure.md#metadata) metadata to match the value baked into `RRSIG(SOA)` by the signer + * online signing is still [planned](#planned) * [Upgrade data structure](doc/ETCD-structure.md#upgrading) (if needed for new program version) without interrupting service * Run [standalone](#standalone-modes) for usage as a [Unix or HTTP connector][pdns-remote-usage] * This could be needed for big data sets, because the initialization from PowerDNS is done lazily (at least as of v4) on first request (which possibly could time out on "big data"…) :-( diff --git a/doc/ETCD-structure.md b/doc/ETCD-structure.md index abfc905..1baaed4 100644 --- a/doc/ETCD-structure.md +++ b/doc/ETCD-structure.md @@ -304,7 +304,7 @@ Metadata keys starting with `X-PE3-` are reserved for use by this backend. They * `X-PE3-FIXED-SERIAL` — when set on a zone, its first value (parsed as an unsigned 32-bit integer per [RFC 1035][rfc1035]) replaces the automatic [zone-revision serial](#soa) in the served `SOA` record. Anything that does not parse (out of range, unparseable, empty) is logged and the automatic serial is used instead. * The main use case is [pre-signed DNSSEC](#pre-signed-dnssec): the served `SOA` serial must match the value the external signer baked into `RRSIG(SOA)`, otherwise validating resolvers reject the answer. * It is also useful without DNSSEC, for operators who want to control the serial manually (e.g. to mirror an existing zone byte-for-byte). -* `X-PE3-MINIMUM-SERIAL` — internal book-keeping. It is added/removed automatically by `setDomainMetadata` transactions to ensure the zone revision (and thus the serial) never moves backwards across a key-delete. Operators normally do not write this key by hand. +* `X-PE3-MINIMUM-SERIAL` — zone serial handling. It is added/removed automatically by transactions to ensure the zone revision (and thus the serial) never moves backwards across a key-delete. Operators should write this key by hand, when they are only deleting keys manually. [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 @@ -315,7 +315,7 @@ there is no need to create or delete them manually. They are not part of the aut ## Pre-signed DNSSEC -pdns-etcd3 supports DNSSEC in the [pre-signed (front-signing)][rfc4035-presigned] model: an external signer (`ldns-signzone`, `dnssec-signzone`, OpenDNSSEC, …) produces the signed records and pushes them into ETCD under the same key layout as ordinary records. The backend itself does not sign anything; [online signing](https://doc.powerdns.com/authoritative/dnssec/modes-of-operation.html) is on the [Planned](../README.md#planned) list. +pdns-etcd3 supports DNSSEC currently only in the [pre-signed (front-signing)][rfc4035-presigned] model: an external signer (`ldns-signzone`, `dnssec-signzone`, OpenDNSSEC, …) produces the signed records and pushes them into ETCD under the same key layout as ordinary records. The backend itself does not sign anything yet; [online signing](https://doc.powerdns.com/authoritative/dnssec/modes-of-operation.html) is on the [Planned](../README.md#planned) list. Setup checklist: @@ -327,7 +327,7 @@ Setup checklist: Operational notes: * Re-signing happens outside the backend. When a re-sign batch lands in ETCD, the zone is reloaded as usual. -* `NSEC`/`NSEC3` chains must remain consistent at all times. Push chain updates atomically (e.g. using a single ETCD transaction or [`setDomainMetadata`-style locks](#locks)) so resolvers never observe a broken denial-of-existence chain. +* `NSEC`/`NSEC3` chains must remain consistent at all times. Push chain updates atomically (e.g. using a single ETCD transaction or [transaction locks](#locks)) so resolvers never observe a broken denial-of-existence chain. * `getDomainKeys` is not implemented — pdns-etcd3 does not store DNSSEC private keys. This is fine for `PRESIGNED=1` zones; it is the missing piece for online signing. [rfc4035-presigned]: https://datatracker.ietf.org/doc/html/rfc4035 diff --git a/src/dnssec_test.go b/src/dnssec_test.go index ea87ede..848301b 100644 --- a/src/dnssec_test.go +++ b/src/dnssec_test.go @@ -49,8 +49,7 @@ func TestFixedSerial(t *testing.T) { if in != nil { dn.metadata[MetaFixedSerial] = in } - params := &rrParams{qtype: "SOA", id: "", data: dn} - return params.fixedSerial(), nil + return soaSerial(dn), nil } checkRun(t, fmt.Sprintf("(%d)%v", i+1, spec.input), tf, spec.input, spec.expected, false) } @@ -80,33 +79,3 @@ func TestSOAFixedSerialThroughProcessValues(t *testing.T) { t.Errorf("SOA content mismatch:\n got: %q\nwant: %q", got.content, want) } } - -// TestDNSSECPlainStringPassthrough: DNSSEC qtypes (no parser, no rrFunc) round-trip verbatim. -func TestDNSSECPlainStringPassthrough(t *testing.T) { - RootLog.ChildLog("data").SetLevel(10) - root := newDataNode(nil, "", "TEST/", false) - zone := root.getChildCreate([]namePart{{"tld", ""}}) - cases := map[string]string{ - "DNSKEY": "257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==", - "RRSIG": "A 13 2 3600 20260601000000 20260501000000 12345 example.com. abc123==", - "NSEC": "host.example.com. A NS SOA MX AAAA RRSIG NSEC DNSKEY", - "NSEC3": "1 0 10 ABCD H9P7U7TR2U91D0V0LJS9L1GIDNP90U3H A RRSIG", - "NSEC3PARAM": "1 0 10 ABCD", - "DS": "12345 13 2 3B1AAAAABBBBCCCC", - "CDS": "0 0 0 00", - "CDNSKEY": "0 3 0 AA==", - } - for qtype, content := range cases { - t.Run(qtype, func(t *testing.T) { - clearMap(zone.records) - zone.processValuesEntry(qtype, "", &valueType{key: qtype, content: stringValueType{s: content}}) - got, ok := zone.records[qtype][""] - if !ok { - t.Fatalf("expected a %s record, got none", qtype) - } - if got.content != content { - t.Errorf("%s content mismatch:\n got: %q\nwant: %q", qtype, got.content, content) - } - }) - } -} diff --git a/src/rr.go b/src/rr.go index 4f8aecc..739b5dd 100644 --- a/src/rr.go +++ b/src/rr.go @@ -267,19 +267,19 @@ func domainName(key string) rrFunc { } } -// fixedSerial returns MetaFixedSerial (if set and a valid uint32) or zoneRev() otherwise. -func (p *rrParams) fixedSerial() int64 { - values, ok := p.data.metadata[MetaFixedSerial] +// soaSerial returns MetaFixedSerial (if set and a valid uint32) or data.zoneRev() otherwise. +func soaSerial(data *dataNode) int64 { + values, ok := data.metadata[MetaFixedSerial] if !ok || len(values) == 0 { - return p.data.zoneRev() + return data.zoneRev() } raw := strings.TrimSpace(values[0]) v, err := strconv.ParseUint(raw, 10, 32) if err != nil { - p.Logf(WarningLevel)("invalid %s metadata, falling back to zoneRev", MetaFixedSerial)("value", raw, "err", err) - return p.data.zoneRev() + data.Logf(WarningLevel)("invalid %s metadata, falling back to zoneRev", MetaFixedSerial)("value", raw, "err", err) + return data.zoneRev() } - p.Logf(3)("using %s metadata as SOA serial", MetaFixedSerial)("serial", v) + data.Logf(3)("using %s metadata as SOA serial", MetaFixedSerial)("serial", v) return int64(v) } @@ -320,7 +320,7 @@ func soa(params *rrParams) { return } // serial: MetaFixedSerial overrides zoneRev (e.g. to match RRSIG(SOA) in pre-signed mode). - serial := params.fixedSerial() + serial := soaSerial(params.data) // refresh refresh, vPath, err := getDuration("refresh", params) if vPath == nil || err != nil {