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:
adamandClaude Opus 5.5 committed 2026-10-13 17:49:49 +01:00
1 parent bfab83da6e
commit fef916b7ab
19 files changed
+587 -122

No files matched your search

+4
View File
@@ -45,6 +45,7 @@ type certInfo struct {
NextAttempt *time.Time `json:"next_attempt,omitempty"`
IssuingSince *time.Time `json:"issuing_since,omitempty"`
Serving bool `json:"serving,omitempty"`
NamedUse []string `json:"named_use,omitempty"`
Files *struct {
Chain string `json:"chain"`
Key string `json:"key"`
@@ -173,6 +174,9 @@ func printCert(w io.Writer, ci certInfo) {
}
}
fmt.Fprintf(w, "Uses:\t%s\n", dash(strings.Join(uses, ", ")))
for _, u := range ci.NamedUse {
fmt.Fprintf(w, "named:\t%s\n", u)
}
if ci.Comment != "" {
fmt.Fprintf(w, "Comment:\t%s\n", ci.Comment)
}
+88 -32
View File
@@ -17,15 +17,17 @@ type encEndpoint struct {
}
type encStatus struct {
Enabled bool `json:"enabled"`
Certificate string `json:"certificate"`
Hostname string `json:"hostname"`
DoTHost string `json:"dot_host"`
DoHURL string `json:"doh_url"`
DoT []encEndpoint `json:"dot"`
DoH []encEndpoint `json:"doh"`
Other bool `json:"other"`
Health *encHealth `json:"health"`
Enabled bool `json:"enabled"`
Certificate string `json:"certificate"`
Hostname string `json:"hostname"`
Hostnames []string `json:"hostnames"`
AddrViews map[string][]string `json:"address_views"`
DoTHost string `json:"dot_host"`
DoHURL string `json:"doh_url"`
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"`
@@ -87,7 +89,14 @@ views.`,
if !s.Enabled {
fmt.Fprintln(w, "Encrypted DNS:\toff (jarl encrypted-dns setup --hostname NAME --address ADDR)")
} else {
fmt.Fprintf(w, "Host names:\t%s\n", strings.Join(s.Hostnames, ", "))
fmt.Fprintf(w, "Certificate:\t%s\n", s.Certificate)
for _, e := range append(append([]encEndpoint{}, s.DoT...), s.DoH...) {
if vs, ok := s.AddrViews[e.Address]; ok {
fmt.Fprintf(w, "Records:\t%s in %s\n", e.Address, strings.Join(vs, ", "))
delete(s.AddrViews, e.Address)
}
}
if len(s.DoT) > 0 {
fmt.Fprintf(w, "DNS over TLS:\t%s\n", endpoints(s.DoT))
}
@@ -119,12 +128,13 @@ views.`,
})
},
}
var host, certName string
var hostList []string
var certName string
var addrs, views []string
var noDoT, noDoH bool
var dohPort, dotPort int
setup := &cobra.Command{
Use: "setup --hostname NAME --address ADDR...",
Use: "setup --hostname NAME... --address ADDR[=VIEW,...]...",
Short: "Set up DNS over TLS and HTTPS (obtaining a certificate if needed)",
Long: `Sets up DoT (port 853) and DoH (port 443, or --doh-port) on the addresses
given, which should be this server's addresses on your own networks:
@@ -135,25 +145,57 @@ given, which should be this server's addresses on your own networks:
- named's tls statement and listen-on statements (and an explicit
listen-on { any; } when there was none, as the first listen-on would
otherwise stop named listening on port 53 everywhere);
- the host name's A and AAAA records, in the private views that have its
zone (or --view), local to the view when the zone is linked.
- the host names' A and AAAA records, local to each view: per address,
the views after = (--address 10.0.10.5=internal,lab), or the views of
--view for every address (default: the private views that have the
zone). A view no longer given loses the address.
It is one change, staged or applied like any other. Running it again
replaces the earlier setup.`,
Several --hostname: the certificate must be for all of them. It is one
change, staged or applied like any other. Running it again replaces the
earlier setup, ports included.`,
Example: ` jarl encrypted-dns setup --hostname dns.adbraun.lol --address 10.0.10.5=internal --address fdab:abab:abab:10::5=internal
jarl encrypted-dns setup --hostname dns.adbraun.lol --hostname doh.adbraun.lol --address 10.0.10.5 --view internal --dot-port 8853`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
host = strings.ToLower(strings.TrimSuffix(host, "."))
if host == "" {
var hosts []string
for _, h := range hostList {
for _, x := range strings.Split(h, ",") {
if x = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(x), ".")); x != "" {
hosts = append(hosts, x)
}
}
}
if len(hosts) == 0 {
return errors.New("give --hostname, the name clients connect to (e.g. dns.adbraun.lol)")
}
host := hosts[0]
var s encStatus
if err := c.Get(ctx(), "/encrypted-dns", q("hostname", host), &s); err != nil {
if err := c.Get(ctx(), "/encrypted-dns", q("hostname", strings.Join(hosts, ",")), &s); err != nil {
return err
}
// ADDR=VIEW,VIEW gives an address its own views.
addrViews := map[string][]string{}
perAddress := false
for i, a := range addrs {
addr, vs, ok := strings.Cut(a, "=")
addrs[i] = strings.TrimSpace(addr)
if ok {
perAddress = true
addrViews[addrs[i]] = nil
for _, v := range strings.Split(vs, ",") {
if v = strings.TrimSpace(v); v != "" {
addrViews[addrs[i]] = append(addrViews[addrs[i]], v)
}
}
}
}
if perAddress && len(views) > 0 {
return errors.New("give the views per address (--address ADDR=VIEW,...) or for all of them (--view), not both")
}
if len(addrs) == 0 {
var list []string
for _, a := range s.Addresses {
@@ -163,20 +205,24 @@ replaces the earlier setup.`,
}
// The certificate.
if certName == "" {
certs:
for _, ci := range s.Certificates {
if coversName(append(ci.Names, ci.Domains...), host) {
certName = ci.Name
break
for _, h := range hosts {
if !coversName(append(ci.Names, ci.Domains...), h) {
continue certs
}
}
certName = ci.Name
break
}
}
if certName == "" {
certName = strings.SplitN(host, ".", 2)[0]
fmt.Printf("Creating certificate %s for %s\n", certName, host)
fmt.Printf("Creating certificate %s for %s\n", certName, strings.Join(hosts, ", "))
var r struct {
Certificate certInfo `json:"certificate"`
}
if err := c.Do(ctx(), "POST", "/certificates", nil, map[string]any{"name": certName, "domains": []string{host}, "uses": []string{"dns"}}, &r); err != nil {
if err := c.Do(ctx(), "POST", "/certificates", nil, map[string]any{"name": certName, "domains": hosts, "uses": []string{"dns"}}, &r); err != nil {
return err
}
if err := waitIssue(c, certName); err != nil {
@@ -204,7 +250,7 @@ replaces the earlier setup.`,
}
}
}
body := map[string]any{"hostname": host, "certificate": certName, "addresses": addrs, "dot": !noDoT, "doh": !noDoH}
body := map[string]any{"hostnames": hosts, "certificate": certName, "addresses": addrs, "dot": !noDoT, "doh": !noDoH}
if cmd.Flags().Changed("doh-port") {
body["doh_port"] = dohPort
}
@@ -234,14 +280,24 @@ replaces the earlier setup.`,
if chk.Error != "" && !flagQuiet {
fmt.Println("Note:", chk.Error)
}
if len(views) == 0 {
for _, v := range s.Views {
if !v.Public && v.HasZone {
views = append(views, v.Name)
switch {
case perAddress:
for _, a := range addrs {
if _, ok := addrViews[a]; !ok {
addrViews[a] = []string{} // no records for it
}
}
body["address_views"] = addrViews
default:
if len(views) == 0 {
for _, v := range s.Views {
if !v.Public && v.HasZone {
views = append(views, v.Name)
}
}
}
body["views"] = views
}
body["views"] = views
resp, err := c.Mutate(ctx(), "POST", "/encrypted-dns", nil, body)
if err != nil {
return err
@@ -250,14 +306,14 @@ replaces the earlier setup.`,
},
}
f := setup.Flags()
f.StringVar(&host, "hostname", "", "the name clients connect to (the certificate's)")
f.StringArrayVar(&hostList, "hostname", nil, "a name clients connect to (repeat, or comma separated, for several; the certificate must be for all)")
f.StringVar(&certName, "certificate", "", "the Jarl certificate to serve (default: one for the host name, obtained if there is none)")
f.StringArrayVar(&addrs, "address", nil, "this server's address to listen on (repeat for several)")
f.StringArrayVar(&addrs, "address", nil, "this server's address to listen on, with the views that get it: 10.0.10.5=internal,lab (repeat for several)")
f.BoolVar(&noDoT, "no-dot", false, "no DNS over TLS")
f.BoolVar(&noDoH, "no-doh", false, "no DNS over HTTPS")
f.IntVar(&dohPort, "doh-port", 443, "port for DNS over HTTPS")
f.IntVar(&dotPort, "dot-port", 853, "port for DNS over TLS (clients expect 853)")
f.StringArrayVar(&views, "view", nil, "views to add the host name's records to (default: the private views that have its zone)")
f.StringArrayVar(&views, "view", nil, "views where every address gets the host names' records (default: the private views that have the zone); per address: --address ADDR=VIEW")
var cAddrs []string
var cDoH int