Stream the delegation check's queries too
release / release (push) Successful in 1m42s

Every DNS query the name server checks send is now reported from
exchangeHostPort as a dig command line with a short answer (addresses,
referral, DS, NXDOMAIN) and its round-trip time; the serials check's SOA
queries keep their own events with the server's result. The delegation
check streams with ?stream=1 like /serials, and the Name servers tab
shows its queries as they happen, keeps its result and follows a running
check when you come back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-09 16:00:39 +00:00
1 parent 1d6c28ba69
commit bb6b4ab64d
6 files changed
+334 -128

No files matched your search

+42 -21
View File
@@ -60,30 +60,12 @@ func (s *Server) zoneSerials(w http.ResponseWriter, r *http.Request, p *auth.Pri
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
// ?stream=1: newline-delimited JSON events while the servers are
// asked (nscheck.Event, kind start/end), then {"kind":"done",
// "response":...}. The web UI shows the queries as they go.
ck := s.checker()
var emit func(any)
if queryBool(r, "stream") {
emit = s.streamJSON(w)
ck.Watch = func(e nscheck.Event) { emit(e) }
}
cmd := s.eng.LoadedSerialCommand(st, z)
if emit != nil && cmd != "" {
emit(nscheck.Event{Kind: "start", ID: 0, Query: cmd})
}
resp.Serial, resp.SerialSource, err = s.localSerial(ctx, st, z)
emit := s.watchChecks(w, r, ck)
resp.Serial, resp.SerialSource, err = s.watchedLocalSerial(ctx, emit, st, z)
if err != nil {
resp.Notes = append(resp.Notes, "named on this server didn't report the zone's serial: "+err.Error())
}
if emit != nil && cmd != "" {
e := nscheck.Event{Kind: "end", ID: 0}
if err != nil {
e.Error = err.Error()
}
emit(e)
}
if z.Signed() && resp.SerialSource == "jarl" {
// named gives the signed zone its own serial, ahead of Jarl's.
resp.Notes = append(resp.Notes, "The zone is signed and named wasn't asked: other servers serve the signed zone's serial, which named sets and which is usually ahead of Jarl's.")
@@ -112,6 +94,40 @@ func (s *Server) zoneSerials(w http.ResponseWriter, r *http.Request, p *auth.Pri
return nil
}
// watchChecks: with ?stream=1, /serials and /delegation answer with
// newline-delimited JSON events while they ask the servers (nscheck.Event,
// kind start/end, one pair per DNS query), then {"kind":"done",
// "response":...}. The web UI shows the queries as they go. It returns
// the function that writes an event, or nil without ?stream=1.
func (s *Server) watchChecks(w http.ResponseWriter, r *http.Request, ck *nscheck.Checker) func(any) {
if !queryBool(r, "stream") {
return nil
}
emit := s.streamJSON(w)
ck.Watch = func(e nscheck.Event) { emit(e) }
return emit
}
// watchedLocalSerial is localSerial, reported as event 0 when streaming.
func (s *Server) watchedLocalSerial(ctx context.Context, emit func(any), st *model.State, z *model.Zone) (*uint32, string, error) {
cmd := s.eng.LoadedSerialCommand(st, z)
if emit != nil && cmd != "" {
emit(nscheck.Event{Kind: "start", ID: 0, Query: cmd})
}
serial, src, err := s.localSerial(ctx, st, z)
if emit != nil && cmd != "" {
e := nscheck.Event{Kind: "end", ID: 0}
switch {
case err != nil:
e.Error = err.Error()
case src == "named" && serial != nil:
e.Answer = fmt.Sprintf("serial %d", *serial)
}
emit(e)
}
return serial, src, err
}
// streamJSON starts a newline-delimited JSON response and returns a
// function that writes one value and flushes it; it is safe for
// concurrent use.
@@ -166,11 +182,16 @@ func (s *Server) zoneDelegation(w http.ResponseWriter, r *http.Request, p *auth.
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
ref, _, _ := s.localSerial(ctx, st, z)
emit := s.watchChecks(w, r, c)
ref, _, _ := s.watchedLocalSerial(ctx, emit, st, z)
resp := api.DelegationResponse{DelegationReport: c.Delegation(ctx, z.Name, ns, addrs, ref, s.signing(ctx, st, z)), View: st.ViewName(z), Notes: []string{}, CheckedAt: time.Now()}
if resp.View != "" {
resp.Notes = append(resp.Notes, fmt.Sprintf("Compared with the zone in view %s. Resolvers on the internet get the view that matches their address; make sure that's this one.", resp.View))
}
if emit != nil {
emit(map[string]any{"kind": "done", "response": resp})
return nil
}
writeJSON(w, http.StatusOK, resp)
return nil
}