option set: statements with a name replace only that one; forwarding over TLS documented

jarl option set tls NAME (and key, remote-servers, primaries, http, dnssec-policy, key-store, server, acl, view, zone, parental-agents) matches the name after the keyword too: setting tls cloudflare no longer replaces tls quad9. jarl option unset tls NAME removes one by its name. docs/encrypted-dns.md has forwarding to Quad9 or Cloudflare over TLS: one changeset, the options explained, several providers, checking it works, and why not over HTTPS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-13 18:39:28 +01:00
1 parent 250e734046
commit ea26171875
6 files changed
+165 -5

No files matched your search

+51 -5
View File
@@ -586,6 +586,12 @@ func optionCmd() *cobra.Command {
Short: "Set an option (replaces an existing option of the same name)",
Long: `Set an option; VALUE is named.conf syntax without the trailing semicolon.
It replaces the option of that name. Statements told apart by the name
after the keyword (tls NAME, key NAME, remote-servers NAME, http NAME,
dnssec-policy NAME, server ADDRESS, ...) replace only the one with that
name: setting tls cloudflare leaves tls quad9 alone. --add adds another
instance of any other option that may occur several times.
jarl option set recursion no
jarl option set allow-recursion '{ homelab; localhost; }'
jarl option set forwarders '{ 1.1.1.1; 9.9.9.9; }'
@@ -610,7 +616,11 @@ func optionCmd() *cobra.Command {
if cmd.Flags().Changed("comment") {
body["comment"] = comment
}
label := optionLabel(strings.ToLower(args[0]), value)
for _, o := range opts {
if label != "" && optionLabel(o.Name, o.Value) != label {
continue // another tls (key, server, ...) statement
}
if o.Name == strings.ToLower(args[0]) && !multiple {
resp, err := c.Mutate(ctx(), "PATCH", fmt.Sprintf("/options/%d", o.ID), nil, body)
if err != nil {
@@ -631,7 +641,7 @@ func optionCmd() *cobra.Command {
set.Flags().StringVar(&comment, "comment", "", "comment")
set.Flags().BoolVar(&multiple, "add", false, "add another instance instead of replacing (for options that may occur several times)")
unset := &cobra.Command{
Use: "unset NAME | unset --id ID",
Use: "unset NAME [LABEL] | unset --id ID",
Short: "Remove an option (BIND's default applies again)",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
@@ -645,8 +655,8 @@ func optionCmd() *cobra.Command {
}
return report(resp)
}
if len(args) != 1 {
return fmt.Errorf("give the option name or --id")
if len(args) < 1 || len(args) > 2 {
return fmt.Errorf("give the option name (and the name after it, for tls NAME and the like) or --id")
}
sc, id, err := scope()
if err != nil {
@@ -658,15 +668,24 @@ func optionCmd() *cobra.Command {
}
var matches []api.Option
for _, o := range opts {
if o.Name == strings.ToLower(args[0]) {
if o.Name == strings.ToLower(args[0]) && (len(args) == 1 || strings.EqualFold(optionLabel(o.Name, o.Value), strings.Trim(args[1], `"`))) {
matches = append(matches, o)
}
}
switch len(matches) {
case 0:
return fmt.Errorf("option %s is not set here", args[0])
return fmt.Errorf("option %s is not set here", strings.Join(args, " "))
case 1:
default:
var labels []string
for _, o := range matches {
if l := optionLabel(o.Name, o.Value); l != "" {
labels = append(labels, l)
}
}
if len(labels) == len(matches) {
return fmt.Errorf("option %s is set %d times (%s); say which: jarl option unset %s %s", args[0], len(matches), strings.Join(labels, ", "), args[0], labels[0])
}
return fmt.Errorf("option %s is set %d times; remove one with --id", args[0], len(matches))
}
resp, err := c.Mutate(ctx(), "DELETE", fmt.Sprintf("/options/%d", matches[0].ID), nil, nil)
@@ -847,3 +866,30 @@ func settingsCmd() *cobra.Command {
cmd.AddCommand(show, set)
return cmd
}
// labelled are the statements told apart by the name after the keyword:
// tls quad9 { ... }, key xfer { ... }, server 192.0.2.1 { ... }.
var labelled = map[string]bool{"tls": true, "http": true, "key": true, "remote-servers": true, "primaries": true, "masters": true,
"parental-agents": true, "dnssec-policy": true, "key-store": true, "server": true, "view": true, "acl": true, "zone": true}
// optionLabel returns the name a labelled statement's value starts with
// ("quad9" for tls quad9 { ... }), lowercase; "" for other statements.
func optionLabel(name, value string) string {
if !labelled[name] {
return ""
}
v := strings.TrimSpace(value)
if v == "" || strings.HasPrefix(v, "{") {
return ""
}
if strings.HasPrefix(v, `"`) {
if end := strings.Index(v[1:], `"`); end >= 0 {
return strings.ToLower(v[1 : end+1])
}
}
end := strings.IndexAny(v, " \t\n{;")
if end < 0 {
end = len(v)
}
return strings.ToLower(v[:end])
}
+17
View File
@@ -232,3 +232,20 @@ func TestOutputFields(t *testing.T) {
}
}
}
func TestOptionLabel(t *testing.T) {
for _, c := range []struct{ name, value, want string }{
{"tls", `quad9 { remote-hostname "dns.quad9.net"; }`, "quad9"},
{"tls", `Cloudflare{ remote-hostname "cloudflare-dns.com"; }`, "cloudflare"},
{"key", `"xfer key" { algorithm hmac-sha256; secret "x"; }`, "xfer key"},
{"server", "192.0.2.1 { keys { xfer; }; }", "192.0.2.1"},
{"remote-servers", "secondaries { 192.0.2.53; }", "secondaries"},
{"forwarders", "{ 9.9.9.9; }", ""},
{"logging", "{ channel x { stderr; }; }", ""},
{"tls", "", ""},
} {
if got := optionLabel(c.name, c.value); got != c.want {
t.Errorf("optionLabel(%q, %q) = %q, want %q", c.name, c.value, got, c.want)
}
}
}