diff --git a/CHANGELOG.md b/CHANGELOG.md index bf29823..393f650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,11 @@ All notable changes to Jarl are documented here. The format follows ### Changed +- `cname_chain` no longer warns about one CNAME pointing at another (a + service name at a host alias is common, and named answers the chain at + once). A chain of three or more CNAMEs gets an info note that shows + it. + - A certificate's private key is only exported over HTTPS, the local socket, or to the server itself (plain HTTP from another host is refused with `https_required`). @@ -70,6 +75,10 @@ All notable changes to Jarl are documented here. The format follows ### Fixed +- The statistics' "Busiest times" grid drew hours without data taller + than the others (they picked up the page's padding for empty + messages). + - A token for one view of a linked zone (such as an ACME token with `views=external`) couldn't add records: a new record is shared, and that reached the other view. A new name and type is now local to the diff --git a/docs/validation.md b/docs/validation.md index 21e4923..1d644a6 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -34,7 +34,7 @@ showing them. | `cname_multiple` | error | A name can have only one CNAME | | `cname_apex` | error | No CNAME at the zone apex (it needs SOA and NS) | | `cname_loop` | error | CNAMEs that point back to themselves | -| `cname_chain` | warning | CNAME to a CNAME: works, but slows resolution | +| `cname_chain` | info | a chain of three or more CNAMEs (one CNAME to another says nothing): works, but each is another lookup for some resolvers | | `cname_dangling` | warning | CNAME to a name that doesn't exist in the zone | | `dname_multiple`, `below_dname` | error | One DNAME per name, and nothing below a DNAME (RFC 6672) | | `apex_ns_missing` | error | A zone needs NS records at its apex | diff --git a/internal/validate/records.go b/internal/validate/records.go index 03bcd08..66fd9a1 100644 --- a/internal/validate/records.go +++ b/internal/validate/records.go @@ -385,8 +385,16 @@ func (c *checker) checkCNAMETarget(z *model.Zone, ix *zoneIndex, r *model.Record if !ix.inZoneTarget(target) { return } - // Follow the chain inside the zone to detect loops. + // Follow the chain inside the zone to detect loops. One CNAME to + // another is common and costs nothing (named answers the chain in + // one response); a longer chain gets a note. seen := map[string]bool{r.Name: true} + chain := []string{disp(r.Name), disp(target)} + note := func() { + if len(chain) > 3 { + c.recProblem(z, r, Info, "cname_chain", "%s reaches its address through %d CNAMEs (%s); fine, but each one is another lookup for resolvers that don't get the chain at once", disp(r.Name), len(chain)-1, strings.Join(chain, " → ")) + } + } cur := target for hops := 0; hops < 20; hops++ { if seen[cur] { @@ -398,18 +406,18 @@ func (c *checker) checkCNAMETarget(z *model.Zone, ix *zoneIndex, r *model.Record if len(next) == 0 { break } - if hops == 0 { - c.recProblem(z, r, Warning, "cname_chain", "%s is a CNAME to %s, which is itself a CNAME; chains slow down resolution", disp(r.Name), disp(target)) - } p := ix.parsed[next[0].ID] if p == nil { break } cur = p.RR.(*dns.CNAME).Target + chain = append(chain, disp(cur)) if !ix.inZoneTarget(cur) { + note() return } } + note() if len(ix.byName[target]) == 0 && !ix.hasAddress(target) && !hasDescendant(ix, target) { c.recProblem(z, r, Warning, "cname_dangling", "%s is a CNAME to %s, which does not exist in the zone", disp(r.Name), disp(target)) } diff --git a/internal/validate/validate_test.go b/internal/validate/validate_test.go index df74447..92d9180 100644 --- a/internal/validate/validate_test.go +++ b/internal/validate/validate_test.go @@ -161,8 +161,9 @@ func TestRecordConflicts(t *testing.T) { }, "cname_loop", Error}, {"cname chain", func(b *builder, z *model.Zone) { b.rec(z, "a", "CNAME", "b") - b.rec(z, "b", "CNAME", "ns1") - }, "cname_chain", Warning}, + b.rec(z, "b", "CNAME", "c") + b.rec(z, "c", "CNAME", "ns1") + }, "cname_chain", Info}, {"dangling cname", func(b *builder, z *model.Zone) { b.rec(z, "a", "CNAME", "nothing") }, "cname_dangling", Warning}, {"null mx not alone", func(b *builder, z *model.Zone) { b.rec(z, "@", "MX", "0 .") @@ -380,3 +381,16 @@ func TestProblemStringDisplaysZone(t *testing.T) { t.Errorf("String() = %q, want %q", got, want) } } + +// One CNAME to another is common and says nothing. +func TestShortCNAMEChain(t *testing.T) { + b := newBuilder() + z := b.primary("example.com", nil) + b.rec(z, "songshare", "CNAME", "navidrome") + b.rec(z, "navidrome", "CNAME", "ns1") + for _, p := range State(b.s, Options{}) { + if p.Code == "cname_chain" { + t.Errorf("one extra CNAME: %s %s", p.Severity, p.Message) + } + } +} diff --git a/internal/web/static/assets/charts.js b/internal/web/static/assets/charts.js index 0624035..4960989 100644 --- a/internal/web/static/assets/charts.js +++ b/internal/web/static/assets/charts.js @@ -283,7 +283,7 @@ export function heatmap(hm, unit) { const n = hm.samples[d][hr]; const text = v < 0 ? day + " " + pad(hr) + ":00 – no data" : day + " " + pad(hr) + ":00 – " + Math.round(v).toLocaleString() + " " + unit + " per hour (average of " + n + (n === 1 ? " hour)" : " hours)"); - const cell = h("div", { class: "hm-cell " + (v < 0 ? "empty" : "heat-" + level(v)), tabindex: "-1", "aria-label": text }); + const cell = h("div", { class: "hm-cell " + (v < 0 ? "hm-empty" : "heat-" + level(v)), tabindex: "-1", "aria-label": text }); const showTip = () => { tip.textContent = text; tip.hidden = false; @@ -313,7 +313,7 @@ export function heatmap(hm, unit) { h("span", { class: "muted small" }, "0"), Array.from({ length: steps }, (_, i) => h("span", { class: "hm-swatch heat-" + i })), h("span", { class: "muted small" }, compact(peak) + " " + unit + "/hour"), - h("span", { class: "hm-swatch empty" }), h("span", { class: "muted small" }, "no data")); + h("span", { class: "hm-swatch hm-empty" }), h("span", { class: "muted small" }, "no data")); return h("div", { class: "heatmap-wrap" }, grid, tip, scale); } diff --git a/internal/web/static/assets/style.css b/internal/web/static/assets/style.css index c6fa914..9082304 100644 --- a/internal/web/static/assets/style.css +++ b/internal/web/static/assets/style.css @@ -398,7 +398,7 @@ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; } .hm-row { display: flex; align-items: center; } .hm-cell { height: 22px; border-radius: 3px; } .hm-cell:hover, .hm-cell:focus { outline: 2px solid var(--text); outline-offset: -1px; } -.hm-cell.empty, .hm-swatch.empty { background: transparent; box-shadow: inset 0 0 0 1px var(--chart-grid); } +.hm-empty { background: transparent; box-shadow: inset 0 0 0 1px var(--chart-grid); } .heat-0 { background: var(--heat-0); } .heat-1 { background: var(--heat-1); } .heat-2 { background: var(--heat-2); } .heat-3 { background: var(--heat-3); } .heat-4 { background: var(--heat-4); } .heat-5 { background: var(--heat-5); } .heat-6 { background: var(--heat-6); }