Encrypted DNS health, comparing a zone's views, --local everywhere

jarld checks the DoT and DoH listeners every 10 minutes as a client would (page, dashboard, jarl encrypted-dns health, jarl_encrypted_dns_up). The Compare views tab and jarl zone diff show what differs between a zone's views, with Use this version. --local works on unlinked zones. A changed certificates.dir is followed by encrypted DNS's tls statements. Sharing an already shared version replaces other views' overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-12 20:46:56 +01:00
1 parent 67ae0091b3
commit 13ffd8cad4
31 files changed
+1153 -18

No files matched your search

+71 -1
View File
@@ -6,6 +6,7 @@ import (
"io"
"net/url"
"strings"
"time"
"github.com/spf13/cobra"
)
@@ -24,6 +25,7 @@ type encStatus struct {
DoT []encEndpoint `json:"dot"`
DoH []encEndpoint `json:"doh"`
Other bool `json:"other"`
Health *encHealth `json:"health"`
Addresses []struct {
Address string `json:"address"`
Private bool `json:"private"`
@@ -99,6 +101,9 @@ views.`,
fmt.Fprintf(w, "For DoH clients:\t%s (Firefox, Chrome, Windows 11)\n", s.DoHURL)
}
}
if s.Enabled {
printHealth(w, s.Health)
}
if s.Other {
fmt.Fprintln(w, "Note:\tnamed has DoT or DoH listeners written by hand too; setup leaves them alone")
}
@@ -308,7 +313,38 @@ replaces the earlier setup.`,
return report(resp)
},
}
cmd.AddCommand(setup, check, disable)
health := &cobra.Command{
Use: "health",
Short: "Check the DoT and DoH listeners now, as a client would",
Long: `Connects to each listener, checks the certificate it serves (for the
host name, current, the one Jarl last wrote) and asks a question. jarld
also does this every 10 minutes; the dashboard and Prometheus
(jarl_encrypted_dns_up) show the result. Exits 1 when a listener doesn't
work.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var h encHealth
if err := c.Do(ctx(), "POST", "/encrypted-dns/health", nil, map[string]any{}, &h); err != nil {
return err
}
if err := output(h, func(w io.Writer) { printHealth(w, &h) }); err != nil {
return err
}
ok := true
for _, p := range h.Probes {
ok = ok && p.OK
}
if !ok {
return errors.New("encrypted DNS isn't working everywhere")
}
return nil
},
}
cmd.AddCommand(setup, check, disable, health)
return cmd
}
@@ -337,3 +373,37 @@ func contains(list []string, s string) bool {
}
return false
}
type encHealth struct {
Checked time.Time `json:"checked"`
Probes []struct {
Kind string `json:"kind"`
Address string `json:"address"`
Port int `json:"port"`
OK bool `json:"ok"`
Problem string `json:"problem"`
Warning string `json:"warning"`
RCode string `json:"rcode"`
Ms int64 `json:"ms"`
} `json:"probes"`
}
func printHealth(w io.Writer, h *encHealth) bool {
if h == nil {
fmt.Fprintln(w, "Health:\tnot checked yet (jarl encrypted-dns health)")
return true
}
ok := true
for _, p := range h.Probes {
kind := map[string]string{"dot": "DoT", "doh": "DoH"}[p.Kind]
state := fmt.Sprintf("working (%s in %d ms)", p.RCode, p.Ms)
if !p.OK {
state, ok = "NOT WORKING: "+p.Problem, false
} else if p.Warning != "" {
state += "; " + p.Warning
}
fmt.Fprintf(w, "%s %s port %d:\t%s\n", kind, p.Address, p.Port, state)
}
fmt.Fprintf(w, "Checked:\t%s\n", h.Checked.Local().Format("2006-01-02 15:04:05"))
return ok
}
+88 -1
View File
@@ -424,7 +424,7 @@ equal to the file: records not in the file are deleted.`,
importCmd.Flags().BoolVar(&replace, "replace", false, "delete records that are not in the file")
importCmd.Flags().BoolVar(&useSOA, "use-soa", false, "also take SOA timers and $TTL from the file")
cmd.AddCommand(list, show, create, update, del, toggle(true), toggle(false), fileCmd, importCmd, zoneSerialsCmd(), zoneDelegationCmd(), zoneDNSSECCmd(), zoneRegistrarCmd(), zoneLinkCmd(true), zoneLinkCmd(false))
cmd.AddCommand(list, show, create, update, del, toggle(true), toggle(false), fileCmd, importCmd, zoneSerialsCmd(), zoneDelegationCmd(), zoneDNSSECCmd(), zoneRegistrarCmd(), zoneLinkCmd(true), zoneLinkCmd(false), zoneDiffCmd())
return cmd
}
@@ -937,3 +937,90 @@ zone is dissolved.`
cmd.Example = " jarl zone unlink adbraun.lol --view vps"
return cmd
}
// zoneDiffCmd is 'zone diff': a zone's records side by side in the views
// that have it.
func zoneDiffCmd() *cobra.Command {
var views []string
var all bool
cmd := &cobra.Command{
Use: "diff ZONE [--view V --view W] [--all]",
Short: "Show what differs between the views that have a zone",
Long: `Lines up a zone's records in the views that have it (or the views
given), one row per name and type, and shows the ones that differ: a
local override in a linked zone, a record one view lacks. With --all, the
ones that are the same too. In a linked zone, 'jarl record update ID
--shared' makes a view's version every view's.`,
Example: ` jarl zone diff adbraun.lol
jarl zone diff adbraun.lol --view internal --view external --all`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
path, qv := zonePath(args[0], "")
if len(views) > 0 {
qv.Set("views", strings.Join(views, ","))
}
if all {
qv.Set("all", "1")
}
var r struct {
Zone string `json:"zone"`
Views []string `json:"views"`
Linked bool `json:"linked"`
Same int `json:"same"`
Rows []struct {
Name string `json:"name"`
Type string `json:"type"`
Same bool `json:"same"`
Views []struct {
Records []string `json:"records"`
IDs []int64 `json:"ids"`
Linked string `json:"linked"`
} `json:"views"`
} `json:"rows"`
}
if err := c.Get(ctx(), path+"/compare", qv, &r); err != nil {
return err
}
return output(r, func(w io.Writer) {
head := []string{"NAME", "TYPE"}
for _, v := range r.Views {
head = append(head, strings.ToUpper(v))
}
fmt.Fprintln(w, strings.Join(head, "\t"))
for _, row := range r.Rows {
cols := []string{row.Name, row.Type}
for _, vs := range row.Views {
cell := "-"
if len(vs.Records) > 0 {
cell = strings.Join(vs.Records, ", ")
if vs.Linked == "local" {
cell += " [local, id " + fmt.Sprint(vs.IDs[0]) + "]"
}
}
cols = append(cols, cell)
}
fmt.Fprintln(w, strings.Join(cols, "\t"))
}
link := "not linked (jarl zone link " + r.Zone + ")"
if r.Linked {
link = "linked"
}
fmt.Fprintf(w, "\n%d names and types differ, %d are the same; %s\n", len(r.Rows)-boolCount(all, r.Same), r.Same, link)
})
},
}
cmd.Flags().StringArrayVar(&views, "view", nil, "a view to compare (repeat; default: every view with the zone)")
cmd.Flags().BoolVar(&all, "all", false, "also show what is the same everywhere")
return cmd
}
func boolCount(b bool, n int) int {
if b {
return n
}
return 0
}