Mark signed zones with a padlock, and name clients in statistics

The zones list shows a padlock after signed zones, linking to their
DNSSEC tab. Client lists in the statistics show the host name Jarl's
records have for each address (PTR, else A/AAAA); no DNS queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-09 23:27:53 +00:00
1 parent 4113d5d6fc
commit 403f85486d
14 files changed
+216 -15

No files matched your search

+95
View File
@@ -2,10 +2,14 @@ package server
import (
"net/http"
"net/netip"
"slices"
"strings"
"time"
"jarl/internal/auth"
"jarl/internal/dnsname"
"jarl/internal/model"
"jarl/internal/stats"
)
@@ -53,6 +57,9 @@ func (s *Server) statsSummary(w http.ResponseWriter, r *http.Request, p *auth.Pr
if err != nil {
return err
}
if err := s.nameClients(r, p, sum.Tops[stats.KindClient]); err != nil {
return err
}
writeJSON(w, http.StatusOK, sum)
return nil
}
@@ -75,6 +82,11 @@ func (s *Server) statsTop(w http.ResponseWriter, r *http.Request, p *auth.Princi
if err != nil {
return badRequest("%v", err)
}
if kind == stats.KindClient || kind == stats.KindRefused {
if err := s.nameClients(r, p, es); err != nil {
return err
}
}
writeJSON(w, http.StatusOK, es)
return nil
}
@@ -145,3 +157,86 @@ func (s *Server) statsBreakdown(w http.ResponseWriter, r *http.Request, p *auth.
writeJSON(w, http.StatusOK, es)
return nil
}
// nameClients gives client entries the host name Jarl's records have for
// the address: the PTR record's target, or else the name of an A/AAAA
// record with the address. It sends no DNS queries, and uses only records
// the user may read. Anonymized clients (prefixes) get no name.
func (s *Server) nameClients(r *http.Request, p *auth.Principal, es []stats.Entry) error {
if len(es) == 0 {
return nil
}
want := map[netip.Addr]bool{}
for _, e := range es {
if a, err := netip.ParseAddr(e.Key); err == nil {
want[a.Unmap()] = true
}
}
if len(want) == 0 {
return nil
}
st, err := s.state(r)
if err != nil {
return err
}
names := clientNames(st, want, func(rec *model.Record) bool { return canReadRecord(p, st, rec) })
for i, e := range es {
if a, err := netip.ParseAddr(e.Key); err == nil {
es[i].Name = names[a.Unmap()]
}
}
return nil
}
// clientNames finds names for the addresses in want (see nameClients).
// Where several records match, the first in name order wins.
func clientNames(st *model.State, want map[netip.Addr]bool, readable func(*model.Record) bool) map[netip.Addr]string {
ptr, host := map[netip.Addr]string{}, map[netip.Addr]string{}
var recs []*model.Record
for _, rec := range st.Records {
if !rec.Disabled && (rec.Type == "PTR" || rec.Type == "A" || rec.Type == "AAAA") {
recs = append(recs, rec)
}
}
slices.SortFunc(recs, func(a, b *model.Record) int {
if a.Name != b.Name {
if model.CanonicalLess(a.Name, b.Name) {
return -1
}
return 1
}
return strings.Compare(a.Data, b.Data)
})
for _, rec := range recs {
var a netip.Addr
var name string
if rec.Type == "PTR" {
var ok bool
if a, ok = dnsname.AddrFromReverse(rec.Name); !ok {
continue
}
name = rec.Data
} else {
var err error
if a, err = netip.ParseAddr(rec.Data); err != nil {
continue
}
name = rec.Name
}
a = a.Unmap()
m := host
if rec.Type == "PTR" {
m = ptr
}
if !want[a] || m[a] != "" || !readable(rec) {
continue
}
m[a] = dnsname.Display(name)
}
for a, n := range host {
if ptr[a] == "" {
ptr[a] = n
}
}
return ptr
}