Widen SOA serial counter: YYMMDDNNNN, 10000 bumps/day
The previous YYYYMMDDNN encoding capped at NN=99 (100 bumps/day) and hard-failed UPDATEs once the day's counter was exhausted — confirmed in production on 2026-05-22 when ACME activity across the supported. systems zone hit the cap and SERVFAILed every subsequent UPDATE. New format: YYMMDD*10000+NNNN. With 4-digit NNNN we get 10000/day, and dropping the century keeps a 2026-dated serial (2,605,229,999 max) under uint32's 4,294,967,295 ceiling. A 4-digit year (e.g., 20260522*10000) would overflow uint32 — RFC 1035's SOA serial type bounds this. Three behavior changes: 1. On NNNN=9999, roll forward to the next encoded day with NNNN=0001 rather than erroring. The encoded date drifts ahead of wall time on heavy churn days and catches up on quiet days; monotonic ordering (the only DNS requirement) holds. 2. Future-encoded serials (from a prior rollover) are honoured — the previous "older date" branch downgraded them back to today*100+1, producing a backwards serial. This bug also tripped a manual workaround on the same day. Now: future encoded dates bump their own NNNN. 3. Legacy YYYYMMDDNN serials migrate automatically on first bump. A value like 2026052299 (~2.026B) is numerically smaller than today's new-format minimum 2605220001 (~2.605B), so the older-or-unparseable branch fires and rewrites in place. New > old, so AXFR receivers treat it as a clean forward bump. Tests cover same-day, rollover, future-encoded no-regress, legacy migration, non-CalVer reset, and no-SOA error.
This commit is contained in:
parent
6268e6eafd
commit
8466f08780
2 changed files with 124 additions and 31 deletions
|
|
@ -121,57 +121,102 @@ func TestAddRRTo_Dedupes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBumpSerial_SameDay_NNIncrement(t *testing.T) {
|
||||
// bumpSerial tests below pin the YYMMDD*10000+NNNN encoding. For
|
||||
// 2026-05-21 (used as `now` in most cases): YYMMDD=260521, baseline
|
||||
// "today, NNNN=0001" = 2,605,210,001.
|
||||
|
||||
func TestBumpSerial_SameDay_NNNNIncrement(t *testing.T) {
|
||||
rrs := []dns.RR{
|
||||
&dns.SOA{Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA},
|
||||
Serial: 2026052105},
|
||||
Serial: 2605210105}, // today, NNNN=0105
|
||||
}
|
||||
// Use a 'now' that matches today's serial date.
|
||||
now := time.Date(2026, 5, 21, 12, 0, 0, 0, time.UTC)
|
||||
if err := bumpSerial(rrs, now); err != nil {
|
||||
t.Fatalf("bumpSerial: %v", err)
|
||||
}
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2026052106 {
|
||||
t.Errorf("Serial = %d, want 2026052106", got)
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2605210106 {
|
||||
t.Errorf("Serial = %d, want 2605210106", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpSerial_NewDay_DateAdvancesNN01(t *testing.T) {
|
||||
func TestBumpSerial_NewDay_DateAdvancesNNNN0001(t *testing.T) {
|
||||
rrs := []dns.RR{
|
||||
&dns.SOA{Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA},
|
||||
Serial: 2026052099}, // exhausted yesterday
|
||||
Serial: 2605209999}, // exhausted yesterday (in new format)
|
||||
}
|
||||
now := time.Date(2026, 5, 21, 1, 0, 0, 0, time.UTC)
|
||||
if err := bumpSerial(rrs, now); err != nil {
|
||||
t.Fatalf("bumpSerial: %v", err)
|
||||
}
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2026052101 {
|
||||
t.Errorf("Serial = %d, want 2026052101 (today, NN=01)", got)
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2605210001 {
|
||||
t.Errorf("Serial = %d, want 2605210001 (today, NNNN=0001)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpSerial_NNExhausted_ReturnsError(t *testing.T) {
|
||||
func TestBumpSerial_NNNN9999_RollsToNextEncodedDay(t *testing.T) {
|
||||
rrs := []dns.RR{
|
||||
&dns.SOA{Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA},
|
||||
Serial: 2026052199}, // today, NN=99
|
||||
Serial: 2605219999}, // today, NNNN=9999 (the burst-day overflow case)
|
||||
}
|
||||
now := time.Date(2026, 5, 21, 1, 0, 0, 0, time.UTC)
|
||||
if err := bumpSerial(rrs, now); err == nil {
|
||||
t.Errorf("expected error on NN=99, got nil")
|
||||
if err := bumpSerial(rrs, now); err != nil {
|
||||
t.Fatalf("bumpSerial: %v", err)
|
||||
}
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2605220001 {
|
||||
t.Errorf("Serial = %d, want 2605220001 (next encoded day, NNNN=0001)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpSerial_FutureEncodedDate_DoesNotRegress(t *testing.T) {
|
||||
// Reproduces the bug from 2026-05-22: an earlier bumpSerial would
|
||||
// downgrade a future-encoded serial back to today's NNNN=0001. Now,
|
||||
// future-encoded serials are honoured: their NNNN increments.
|
||||
rrs := []dns.RR{
|
||||
&dns.SOA{Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA},
|
||||
Serial: 2605220500}, // tomorrow (encoded), NNNN=0500
|
||||
}
|
||||
now := time.Date(2026, 5, 21, 1, 0, 0, 0, time.UTC)
|
||||
if err := bumpSerial(rrs, now); err != nil {
|
||||
t.Fatalf("bumpSerial: %v", err)
|
||||
}
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2605220501 {
|
||||
t.Errorf("Serial = %d, want 2605220501 (future date NNNN+1, no regression)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpSerial_LegacyYYYYMMDDNNFormat_MigratesForward(t *testing.T) {
|
||||
// The migration path: any production zone whose SOA serial is in the
|
||||
// old 10-digit YYYYMMDDNN format (e.g., 2026052299) gets numerically
|
||||
// crushed by today's YYMMDDNNNN minimum, so the "older/unparseable"
|
||||
// branch fires and rewrites in place.
|
||||
rrs := []dns.RR{
|
||||
&dns.SOA{Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA},
|
||||
Serial: 2026052299}, // legacy YYYYMMDDNN, exhausted day
|
||||
}
|
||||
now := time.Date(2026, 5, 22, 12, 0, 0, 0, time.UTC) // 2026-05-22
|
||||
if err := bumpSerial(rrs, now); err != nil {
|
||||
t.Fatalf("bumpSerial: %v", err)
|
||||
}
|
||||
got := rrs[0].(*dns.SOA).Serial
|
||||
if got != 2605220001 {
|
||||
t.Errorf("Serial = %d, want 2605220001 (migrated to new format)", got)
|
||||
}
|
||||
if got <= 2026052299 {
|
||||
t.Errorf("migration regressed serial: %d <= legacy %d", got, 2026052299)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpSerial_NonCalVerFormat_ResetsToToday(t *testing.T) {
|
||||
rrs := []dns.RR{
|
||||
&dns.SOA{Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA},
|
||||
Serial: 12345}, // random unix-ish serial, not CalVer
|
||||
Serial: 12345}, // random small value, not CalVer
|
||||
}
|
||||
now := time.Date(2026, 5, 21, 1, 0, 0, 0, time.UTC)
|
||||
if err := bumpSerial(rrs, now); err != nil {
|
||||
t.Fatalf("bumpSerial: %v", err)
|
||||
}
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2026052101 {
|
||||
t.Errorf("Serial = %d, want 2026052101", got)
|
||||
if got := rrs[0].(*dns.SOA).Serial; got != 2605210001 {
|
||||
t.Errorf("Serial = %d, want 2605210001", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue