package bind import ( "context" "encoding/hex" "fmt" "strconv" "strings" ) // JournalPath returns the BIND journal that accompanies a zone database file. func JournalPath(zonePath string) string { return zonePath + ".jnl" } // ZoneDiskState is what the primary pod's filesystem holds for one zone. // A journal is only replayable onto a zone file whose SOA serial lies within // [JournalBegin, JournalEnd]; outside that range BIND fails the load with // "out of range" and the zone never comes up. type ZoneDiskState struct { ZoneFile bool ZoneSerial int64 ZoneSerialOK bool Journal bool JournalBegin int64 JournalEnd int64 JournalOK bool } // SeedPlan is the decision taken before writing a skeleton zone file. type SeedPlan struct { WriteSeed bool Serial int64 QuarantineZoneFile bool QuarantineJournal bool QuarantineSuffix string } // PlanSeed decides how to make a zone loadable without discarding live data. // Nothing is ever deleted: unusable files are renamed aside so they stay // recoverable on the PVC. func PlanSeed(st ZoneDiskState) SeedPlan { suffix := fmt.Sprintf(".orphaned-%d", highestSerial(st)) if !st.ZoneFile { p := SeedPlan{WriteSeed: true, Serial: nextSerial(st), QuarantineSuffix: suffix} // A journal without its zone file can only refuse to replay. p.QuarantineJournal = st.Journal return p } if !st.Journal || !st.JournalOK || !st.ZoneSerialOK { return SeedPlan{} } switch { case serialLT(st.ZoneSerial, st.JournalBegin): // The file has regressed behind the journal: it is the skeleton a // previous reconcile clobbered it with. Keep both aside and reseed // above the journal so secondaries still see a serial increase. return SeedPlan{ WriteSeed: true, Serial: nextSerial(st), QuarantineZoneFile: true, QuarantineJournal: true, QuarantineSuffix: suffix, } case serialLT(st.JournalEnd, st.ZoneSerial): return SeedPlan{QuarantineJournal: true, QuarantineSuffix: suffix} default: return SeedPlan{} } } func highestSerial(st ZoneDiskState) int64 { var h int64 if st.ZoneFile && st.ZoneSerialOK { h = st.ZoneSerial } if st.Journal && st.JournalOK && serialLT(h, st.JournalEnd) { h = st.JournalEnd } return h } func nextSerial(st ZoneDiskState) int64 { next := int64(uint32(highestSerial(st)) + 1) if next == 0 { return 1 } return next } // serialLT compares DNS serials in RFC 1982 sequence space. func serialLT(a, b int64) bool { if a == b { return false } return uint32(b)-uint32(a) < 1<<31 } // ParseZoneSerial extracts the SOA serial from the head of a zone file, in // both the operator's seed layout and BIND's own multi-line dump layout. func ParseZoneSerial(content string) (int64, bool) { var tokens []string for _, line := range strings.Split(content, "\n") { if i := strings.IndexByte(line, ';'); i >= 0 { line = line[:i] } line = strings.ReplaceAll(line, "(", " ( ") line = strings.ReplaceAll(line, ")", " ) ") tokens = append(tokens, strings.Fields(line)...) } for i, tok := range tokens { if !strings.EqualFold(tok, "SOA") { continue } rest := tokens[i+1:] if len(rest) < 3 { return 0, false } rest = rest[2:] // MNAME, RNAME if rest[0] == "(" { rest = rest[1:] } if len(rest) == 0 { return 0, false } serial, err := strconv.ParseUint(rest[0], 10, 32) if err != nil { return 0, false } return int64(serial), true } return 0, false } const journalMagic = ";BIND LOG V9" // parseJournalHeader reads the begin and end serials from a BIND journal // header: a 16-byte format magic followed by two {serial,offset} big-endian // pairs. func parseJournalHeader(b []byte) (begin, end int64, ok bool) { if len(b) < 28 || !strings.HasPrefix(string(b[:16]), journalMagic) { return 0, 0, false } return int64(beUint32(b[16:20])), int64(beUint32(b[24:28])), true } func beUint32(b []byte) uint32 { return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) } // zoneStateProbe reads only the head of the zone file: the SOA is the first // record in both layouts the operator has to read. func zoneStateProbe(path string) string { jnl := JournalPath(path) return strings.Join([]string{ fmt.Sprintf("if [ -f '%s' ]; then printf 'zonefile=1\\nhead<<\\n'; head -c 4096 '%s'; printf '\\n>>head\\n'; else printf 'zonefile=0\\n'; fi", path, path), fmt.Sprintf("if [ -f '%s' ]; then printf 'journal=1\\njnl=%%s\\n' \"$(od -An -v -tx1 -N32 '%s' | tr -d ' \\n')\"; else printf 'journal=0\\n'; fi", jnl, jnl), }, "\n") } func parseZoneDiskState(out string) ZoneDiskState { var st ZoneDiskState var head []string inHead := false for _, line := range strings.Split(out, "\n") { switch { case inHead && line == ">>head": inHead = false case inHead: head = append(head, line) case line == "head<<": inHead = true case line == "zonefile=1": st.ZoneFile = true case line == "journal=1": st.Journal = true case strings.HasPrefix(line, "jnl="): if raw, err := hex.DecodeString(strings.TrimPrefix(line, "jnl=")); err == nil { st.JournalBegin, st.JournalEnd, st.JournalOK = parseJournalHeader(raw) } } } if st.ZoneFile { st.ZoneSerial, st.ZoneSerialOK = ParseZoneSerial(strings.Join(head, "\n")) } return st } // ZoneDiskState inspects the zone database file and journal on the pod. func (e *Executor) ZoneDiskState(ctx context.Context, namespace, pod, path string) (ZoneDiskState, error) { out, err := e.Exec(ctx, namespace, pod, []string{"sh", "-c", zoneStateProbe(path)}, "") if err != nil { return ZoneDiskState{}, fmt.Errorf("inspect zone files %s: %w (out: %s)", path, err, out) } return parseZoneDiskState(out), nil } // Quarantine renames the files the plan marks unusable, leaving them on the // PVC under a suffixed name. func (e *Executor) Quarantine(ctx context.Context, namespace, pod, path string, plan SeedPlan) error { var cmds []string if plan.QuarantineZoneFile { cmds = append(cmds, moveAside(path, plan.QuarantineSuffix)) } if plan.QuarantineJournal { cmds = append(cmds, moveAside(JournalPath(path), plan.QuarantineSuffix)) } if len(cmds) == 0 { return nil } cmd := []string{"sh", "-c", strings.Join(cmds, "\n")} if out, err := e.Exec(ctx, namespace, pod, cmd, ""); err != nil { return fmt.Errorf("quarantine zone files %s: %w (out: %s)", path, err, out) } return nil } func moveAside(path, suffix string) string { return fmt.Sprintf("if [ -f '%s' ]; then mv -- '%s' '%s%s'; fi", path, path, path, suffix) }