L1/L2/L4: cleanup + README operational guide
L1 — Replace hand-rolled atoi/parseUint with strconv.ParseUint wrapped
in mustParseUint. Hamilton's reasoning: the comment "strconv adds
overhead we don't need" is the Lauren-Bug shape — we already validated
the input. Until we hadn't, on a path we couldn't predict. Stdlib's
edge-case coverage is the safer default; the wrapper panics on
malformed input so any future regression surfaces in CI, not as a
silent 0 serial.
L2 — applyUpdate no longer mutates the caller's RR header TTL. miekg/
dns parses the UPDATE message into RRs the caller still owns; silently
rewriting hdr.Ttl was a hygiene smell with no current functional
consequence but a clear documentation issue. Now we dns.Copy() the RR
before any header mutation.
L4 — README expanded with an "Operational constraints" section
documenting the contracts and limits operators should understand
before relying on this in production:
- Single-process atomicity only (with rsync-race mitigation)
- Process-global MsgAcceptFunc override
- No-op UPDATE doesn't bump SOA (with touch-UPDATE workaround)
- SOA invariants enforced strictly (zero, multi, non-apex SOA all
refused)
- Serial counter NNNN=9999 rollover semantics
- TSIG replay window dependency on miekg/dns default
- Git commit failure logged at ERROR, not rolled back
- Per-key rate limit knobs
Every constraint maps to a Hamilton review finding; documenting the
contract in operator-facing prose closes the gap between code and
expectation that the review identified.
This commit is contained in:
parent
8d1477350a
commit
89993ca207
3 changed files with 120 additions and 28 deletions
40
zonefile.go
40
zonefile.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -346,9 +347,9 @@ func bumpSerial(rrs []dns.RR, now time.Time) error {
|
|||
// serial that happens to look like a valid YYMMDD prefix but is in
|
||||
// the past, which is the same handling: jump to today).
|
||||
if curDate := cur[:6]; isValidYYMMDD(curDate) && curDate >= today {
|
||||
nnnn := atoi(cur[6:10])
|
||||
nnnn := int(mustParseUint(cur[6:10]))
|
||||
if nnnn < 9999 {
|
||||
soa.Serial = uint32(parseUint(curDate)*serialCounterMul + uint64(nnnn+1))
|
||||
soa.Serial = uint32(mustParseUint(curDate)*serialCounterMul + uint64(nnnn+1))
|
||||
return nil
|
||||
}
|
||||
// NNNN=9999: roll to next encoded day, NNNN=0001.
|
||||
|
|
@ -357,13 +358,13 @@ func bumpSerial(rrs []dns.RR, now time.Time) error {
|
|||
return fmt.Errorf("serial date %q unparseable: %w", curDate, err)
|
||||
}
|
||||
next := d.AddDate(0, 0, 1).Format("060102")
|
||||
soa.Serial = uint32(parseUint(next)*serialCounterMul + 1)
|
||||
soa.Serial = uint32(mustParseUint(next)*serialCounterMul + 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Older or unparseable: jump to today*10000+1. Migration path for
|
||||
// legacy YYYYMMDDNN serials lives here.
|
||||
candidate := uint32(parseUint(today)*serialCounterMul + 1)
|
||||
candidate := uint32(mustParseUint(today)*serialCounterMul + 1)
|
||||
|
||||
// H5 — explicit MaxUint32 guard. Plain `>` comparison is correct in
|
||||
// practice (we'd never wrap during the zone's lifetime: 10000
|
||||
|
|
@ -399,23 +400,20 @@ func isValidYYMMDD(s string) bool {
|
|||
return err == nil
|
||||
}
|
||||
|
||||
// atoi is a tiny helper that ignores errors — only called on a
|
||||
// substring we already validated is two digits.
|
||||
func atoi(s string) int {
|
||||
n := 0
|
||||
for _, c := range s {
|
||||
n = n*10 + int(c-'0')
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// parseUint parses an all-digits string into a uint64. Used because
|
||||
// strconv.ParseUint adds error-handling overhead we don't need on
|
||||
// internally-controlled inputs.
|
||||
func parseUint(s string) uint64 {
|
||||
var n uint64
|
||||
for _, c := range s {
|
||||
n = n*10 + uint64(c-'0')
|
||||
// mustParseUint parses an all-digit string into uint64. Panics on
|
||||
// malformed input — the caller is responsible for passing only strings
|
||||
// that were validated as digit-substrings (e.g., a fixed-width slice of
|
||||
// a YYMMDDNNNN-formatted serial). Using strconv via this thin wrapper
|
||||
// keeps the panic behavior explicit while sharing stdlib's robust
|
||||
// parsing (Hamilton L1).
|
||||
func mustParseUint(s string) uint64 {
|
||||
n, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
// Programmer error if we ever hit this — every caller passes
|
||||
// digits-only strings derived from time.Format or a sliced
|
||||
// SOA serial. Panic so the bug surfaces in tests/CI rather
|
||||
// than silently producing a 0 serial.
|
||||
panic(fmt.Sprintf("mustParseUint(%q): %v", s, err))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue