package engine import ( "context" "fmt" "time" "jarl/internal/model" "jarl/internal/render" "jarl/internal/validate" ) // serialGreater reports whether a > b in RFC 1982 serial number // arithmetic. func serialGreater(a, b uint32) bool { return int32(a-b) > 0 } // NextSerial returns the serial after old under policy, with the same // semantics as BIND's serial-update-method. func NextSerial(old uint32, policy string, now time.Time) uint32 { switch policy { case model.SerialUnixtime: t := uint32(now.Unix()) if serialGreater(t, old) { return t } return old + 1 case model.SerialIncrement: return old + 1 default: // date: YYYYMMDDnn d := dateSerial(now) if serialGreater(d, old) { return d } return old + 1 } } // SerialChange records a serial bump. type SerialChange struct { ZoneID int64 `json:"zone_id"` Zone string `json:"zone"` View string `json:"view,omitempty"` Old uint32 `json:"old"` New uint32 `json:"new"` } // bumpSerials increments the serial of every primary zone in work whose // DNS data differs from base, unless the serial was set explicitly. func bumpSerials(base, work *model.State, now time.Time, user string) []SerialChange { var out []SerialChange for _, z := range work.SortedZones() { if z.Type != model.ZonePrimary { continue } policy := z.SerialPolicy if policy == "" { policy = work.Settings.SerialPolicy } old := base.Zones[z.ID] if old == nil || old.Type != model.ZonePrimary || old.ViewID != z.ViewID { // New to named (or moved): keep the serial it was created // with, which InitialSerial or the source zone provided. continue } if z.SOA.Serial != old.SOA.Serial { // Set explicitly by the user. continue } if render.ZoneContentHash(base, old) == render.ZoneContentHash(work, z) { continue } next := NextSerial(old.SOA.Serial, policy, now) out = append(out, SerialChange{ZoneID: z.ID, Zone: z.Name, View: work.ViewName(z), Old: old.SOA.Serial, New: next}) z.SOA.Serial = next z.Touch(user, now) } return out } // guardSerials makes sure every zone whose file is about to change gets // a serial above the one named has loaded. Normally Jarl's serial is // already higher, but not after restoring an older database or when the // files on disk were replaced: named refuses to load a signed zone's file // unless its serial went up (and keeps serving the old data), and // secondaries ignore a zone whose serial went down. Zones are bumped past // named's serial unless the user set the serial explicitly; an explicit // serial that a signed zone can't take is an error. named is only asked // when it controls the zone (not with no_reload). func (e *Engine) guardSerials(ctx context.Context, base, work *model.State, bumped []SerialChange, user string) ([]SerialChange, []validate.Problem) { if e.cfg.NoReload || e.tools == nil { return nil, nil } auto := map[int64]bool{} for _, c := range bumped { auto[c.ZoneID] = true } var out []SerialChange var problems []validate.Problem for _, z := range work.SortedZones() { if z.Type != model.ZonePrimary { continue } old := base.Zones[z.ID] if old == nil || old.Type != model.ZonePrimary || old.ViewID != z.ViewID { continue // new to named: nothing loaded to compare with } path := render.ZoneFilePath(work, z, e.cfg.Paths) if cur, ok, _ := readFile(path); ok && string(cur) == render.ZoneFile(work, z) { continue // named keeps what it has } zs, err := e.tools.ZoneStatus(ctx, z.Name, e.namedView(work, z)) if err != nil || !zs.HasSerial || serialGreater(z.SOA.Serial, zs.Serial) { continue } explicit := z.SOA.Serial != old.SOA.Serial && !auto[z.ID] if explicit { if z.Signed() { problems = append(problems, validate.Problem{Severity: validate.Error, Code: "serial_not_higher", ObjectType: "zone", ObjectID: z.ID, Zone: z.Name, View: work.ViewName(z), Message: fmt.Sprintf("named only loads a signed zone's file when its serial goes up, and it has serial %d loaded; set a higher serial (or leave it to Jarl)", zs.Serial)}) } continue } policy := z.SerialPolicy if policy == "" { policy = work.Settings.SerialPolicy } next := NextSerial(zs.Serial, policy, e.Now()) out = append(out, SerialChange{ZoneID: z.ID, Zone: z.Name, View: work.ViewName(z), Old: z.SOA.Serial, New: next}) e.log.Info("raising a zone's serial above the one named has loaded", "zone", z.Name, "view", work.ViewName(z), "jarl", z.SOA.Serial, "named", zs.Serial, "new", next) z.SOA.Serial = next z.Touch(user, e.Now()) } return out, problems }