Encrypted DNS: several host names, views per listen address, what named uses a certificate for
The setup takes several host names (one certificate for all; the health check says when the served certificate misses one), stored in the settings. Each listen address gets its own views for the host names' A/AAAA records (--address ADDR=VIEW,..., a view checkbox per address in the form), always local to the view, with a warning when a public view would get a private address; a view no longer given loses the address, other records of the names stay, and the form starts from where the records are now. Certificates (page and jarl cert show) say what named uses each for: DNS over TLS or HTTPS, on which addresses and ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bfab83da6e
commit
fef916b7ab
19 files changed
+587
-122
No files matched your search
@@ -9,6 +9,7 @@ import (
|
||||
"jarl/internal/api"
|
||||
"jarl/internal/auth"
|
||||
"jarl/internal/certs"
|
||||
"jarl/internal/encdns"
|
||||
"jarl/internal/model"
|
||||
)
|
||||
|
||||
@@ -21,6 +22,9 @@ type certView struct {
|
||||
NextTry *time.Time `json:"next_attempt,omitempty"` // acme: when the loop obtains it next
|
||||
Files *certFiles `json:"files,omitempty"` // dns use: where named reads it
|
||||
Serving bool `json:"serving,omitempty"` // jarld serves it now (web use)
|
||||
// NamedUse says what of named's configuration uses it: "DNS over TLS
|
||||
// on 10.0.10.5 port 853" (list and get only).
|
||||
NamedUse []string `json:"named_use,omitempty"`
|
||||
// IssuingSince is set while Jarl is obtaining it.
|
||||
IssuingSince *time.Time `json:"issuing_since,omitempty"`
|
||||
}
|
||||
@@ -79,9 +83,15 @@ func (s *Server) certList(w http.ResponseWriter, r *http.Request, p *auth.Princi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st, err := s.store.LoadState(r.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := []certView{}
|
||||
for _, c := range all {
|
||||
out = append(out, s.presentCert(c))
|
||||
v := s.presentCert(c)
|
||||
v.NamedUse = encdns.CertUses(st, s.cfg.Certs.FilesDir(c.Name))
|
||||
out = append(out, v)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"certificates": out, "account": s.cfg.Certs.AccountInfo(r.Context())})
|
||||
return nil
|
||||
@@ -96,7 +106,13 @@ func (s *Server) certGet(w http.ResponseWriter, r *http.Request, p *auth.Princip
|
||||
if err != nil {
|
||||
return certErr(err)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, s.presentCert(c))
|
||||
st, err := s.store.LoadState(r.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := s.presentCert(c)
|
||||
v.NamedUse = encdns.CertUses(st, s.cfg.Certs.FilesDir(c.Name))
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+41
-10
@@ -39,8 +39,13 @@ type encCert struct {
|
||||
|
||||
type encStatus struct {
|
||||
encdns.Status
|
||||
// Hostname is the name clients connect to (the certificate's first).
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
// Hostnames are the names clients connect to (the setup's, else the
|
||||
// certificate's first); Hostname is the first.
|
||||
Hostnames []string `json:"hostnames"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
// AddressViews are, per address, the views whose records give a host
|
||||
// name that address.
|
||||
AddressViews map[string][]string `json:"address_views"`
|
||||
// DoTHost and DoHURL are what to give clients.
|
||||
DoTHost string `json:"dot_host,omitempty"`
|
||||
DoHURL string `json:"doh_url,omitempty"`
|
||||
@@ -92,6 +97,15 @@ func (s *Server) encryptedDNS(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
||||
}
|
||||
}
|
||||
}
|
||||
if hs := st.Settings.EncryptedDNSHosts; len(hs) > 0 {
|
||||
out.Hostname = hs[0]
|
||||
out.Hostnames = hs
|
||||
} else if out.Hostname != "" {
|
||||
out.Hostnames = []string{out.Hostname}
|
||||
}
|
||||
if out.Hostnames == nil {
|
||||
out.Hostnames = []string{}
|
||||
}
|
||||
if out.Hostname != "" {
|
||||
if len(out.DoT) > 0 {
|
||||
out.DoTHost = out.Hostname
|
||||
@@ -120,17 +134,25 @@ func (s *Server) encryptedDNS(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
||||
out.Addresses = append(out.Addresses, encAddress{Address: a.String(), Private: encdns.Private(a)})
|
||||
}
|
||||
slices.SortFunc(out.Addresses, func(a, b encAddress) int { return strings.Compare(a.Address, b.Address) })
|
||||
host := strings.TrimSuffix(r.URL.Query().Get("hostname"), ".")
|
||||
if host == "" {
|
||||
host = out.Hostname
|
||||
hosts := out.Hostnames
|
||||
if q := r.URL.Query().Get("hostname"); q != "" {
|
||||
hosts = nil
|
||||
for _, h := range strings.Split(q, ",") {
|
||||
if h = strings.TrimSuffix(strings.TrimSpace(h), "."); h != "" {
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Where the records of these host names give each address now, the
|
||||
// setup's or not: what the form starts from.
|
||||
out.AddressViews = encdns.CurrentAddressViews(st, hosts)
|
||||
for _, x := range s.eng.Exposures(st) {
|
||||
if x.View == "" {
|
||||
continue
|
||||
}
|
||||
v := encView{Name: x.View, Public: x.Public}
|
||||
if host != "" {
|
||||
if vv := st.ViewByName(x.View); vv != nil {
|
||||
if vv := st.ViewByName(x.View); vv != nil {
|
||||
for _, host := range hosts {
|
||||
for _, z := range st.ZonesInView(vv.ID) {
|
||||
if z.Type == model.ZonePrimary && (host+"." == z.Name || strings.HasSuffix(host+".", "."+z.Name)) {
|
||||
v.HasZone = true
|
||||
@@ -204,13 +226,17 @@ func (s *Server) encryptedDNSSetup(w http.ResponseWriter, r *http.Request, p *au
|
||||
return badRequest("certificate %s doesn't have the dns use, so named can't read it: jarl cert update %s --use dns (and keep its other uses)", c.Name, c.Name)
|
||||
case c.NotAfter == nil:
|
||||
return conflict("certificate %s hasn't been obtained yet (%s); set up encrypted DNS once it has", c.Name, s.cfg.Certs.StatusOf(c).Message)
|
||||
case in.Hostname != "" && !certs.Covers(c.Names, []string{in.Hostname}):
|
||||
return badRequest("certificate %s is for %s, not %s: clients would reject it", c.Name, strings.Join(c.Names, ", "), in.Hostname)
|
||||
case len(in.Hosts()) > 0 && !certs.Covers(c.Names, in.Hosts()):
|
||||
return badRequest("certificate %s is for %s, not all of %s: clients would reject it (add the names to it: jarl cert update %s --domain …)", c.Name, strings.Join(c.Names, ", "), strings.Join(in.Hosts(), ", "), c.Name)
|
||||
}
|
||||
st, err := s.store.LoadState(r.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.PublicViews = map[string]bool{}
|
||||
for _, x := range s.eng.Exposures(st) {
|
||||
in.PublicViews[x.View] = x.Public
|
||||
}
|
||||
reqs, notes, err := encdns.Plan(st, in, s.cfg.Certs.FilesDir(""))
|
||||
if err != nil {
|
||||
return badRequest("%v", err)
|
||||
@@ -222,7 +248,7 @@ func (s *Server) encryptedDNSSetup(w http.ResponseWriter, r *http.Request, p *au
|
||||
if in.DoH {
|
||||
what = append(what, "DNS over HTTPS")
|
||||
}
|
||||
return s.mutateNotes(w, r, p, "set up "+strings.Join(what, " and ")+" for "+in.Hostname, reqs, nil, notes)
|
||||
return s.mutateNotes(w, r, p, "set up "+strings.Join(what, " and ")+" for "+strings.Join(in.Hosts(), ", "), reqs, nil, notes)
|
||||
}
|
||||
|
||||
// encryptedDNSDisable: DELETE /encrypted-dns.
|
||||
@@ -274,7 +300,12 @@ func (s *Server) checkEncryptedDNS(ctx context.Context) *encdns.Health {
|
||||
var h *encdns.Health
|
||||
if st.Enabled {
|
||||
if host, serial := s.encTarget(ctx, st); host != "" {
|
||||
hosts := state.Settings.EncryptedDNSHosts
|
||||
if len(hosts) > 0 {
|
||||
host = hosts[0]
|
||||
}
|
||||
h = encdns.Check(ctx, st, host, serial, s.encRoots)
|
||||
h.RequireNames(append([]string{host}, hosts...))
|
||||
}
|
||||
}
|
||||
s.encHealth.Lock()
|
||||
|
||||
Reference in new issue
Block a user