dns: pick the extra_records_path format by file extension

HuJSON and YAML records work too; an unknown extension fails instead of being
parsed as JSON.
This commit is contained in:
Kristoffer Dalby
2026-09-28 13:33:22 +00:00
committed by Kristoffer Dalby
parent dd8ce695cf
commit 906016beb4
4 changed files with 12 additions and 9 deletions
+2
View File
@@ -79,6 +79,7 @@ removed on this schedule:
#### Configuration
- `derp.paths` files must end in `.yaml`, `.yml`, `.json` or `.hujson`; the extension picks the format
- `dns.extra_records_path` must end in `.json`, `.hujson`, `.yaml` or `.yml`; the extension picks the format
- A `derp.paths` file that decodes to no regions now stops headscale from starting instead of being silently ignored
#### CLI
@@ -94,6 +95,7 @@ removed on this schedule:
- Deleting a user that still owns nodes now lists the nodes (ID and hostname) that must be deleted first [#3475](https://github.com/juanfont/headscale/pull/3475)
- Headscale now requires Go 1.27 to build
- `derp.paths` files may be Tailscale JSON or HuJSON DERP maps as well as YAML
- `dns.extra_records_path` files may be HuJSON or YAML as well as JSON
- A `derp.paths` region set to `null` removes that region again, as documented
## 0.29.4 (2026-09-23)
+2 -1
View File
@@ -353,7 +353,8 @@ dns:
# # you can also put it in one line
# - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
#
# Alternatively, extra DNS records can be loaded from a JSON file.
# Alternatively, extra DNS records can be loaded from a file. The extension
# picks the format: .json, .hujson, .yaml or .yml.
# Headscale processes this file on each change.
# extra_records_path: /var/lib/headscale/extra-records.json
+1 -1
View File
@@ -67,7 +67,7 @@ hostname and port combination "http://hostname-in-magic-dns.myvpn.example.com:30
!!! tip "Good to know"
- The `dns.extra_records_path` option in the [configuration file](configuration.md) needs to reference the
JSON file containing extra DNS records.
file containing extra DNS records. Its extension picks the format: `.json`, `.hujson`, `.yaml` or `.yml`.
- Be sure to "sort keys" and produce a stable output in case you generate the JSON file with a script.
Headscale uses a checksum to detect changes to the file and a stable output avoids unnecessary processing.
+7 -7
View File
@@ -3,7 +3,6 @@ package dns
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"os"
@@ -12,6 +11,7 @@ import (
"github.com/cenkalti/backoff/v5"
"github.com/fsnotify/fsnotify"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
"tailscale.com/util/set"
@@ -222,8 +222,9 @@ func (e *ExtraRecordsMan) updateRecords() {
}
}
// readExtraRecordsFromPath reads a JSON file of [tailcfg.DNSRecord]
// and returns the records and the hash of the file.
// readExtraRecordsFromPath reads a file of [tailcfg.DNSRecord] in the format its
// extension names (see [util.UnmarshalByExt]) and returns the records and the
// hash of the file.
func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byte, error) {
var zero [32]byte
@@ -238,14 +239,13 @@ func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byte, error
return nil, zero, nil
}
var records []tailcfg.DNSRecord
// Hash first: decoding HuJSON may rewrite comments in b.
hash := sha256.Sum256(b)
err = json.Unmarshal(b, &records)
records, err := util.UnmarshalByExt[[]tailcfg.DNSRecord](path, b)
if err != nil {
return nil, zero, fmt.Errorf("unmarshalling records, content: %q: %w", string(b), err)
}
hash := sha256.Sum256(b)
return records, hash, nil
}