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
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user