Jarl 0.1.0: manage BIND through a web UI, REST API and CLI

Jarl keeps a BIND 9.20 server's whole configuration (views, ACLs, TSIG
keys, zones of every type, records, named.conf options) in SQLite and
renders named.conf, keys.conf and zone files from it.

Every change goes through one changeset pipeline: whole-configuration
validation, named-checkconf -z and named-checkzone in a staging
directory, atomic install, rndc reconfig/reload, serial verification
with rndc zonestatus, and rollback if named does not load the change.
Changes apply one at a time by default or are bundled and applied
together. Applied changesets can be reverted.

- jarld: the daemon with the REST API and an embedded web UI, plus import,
  adopt, check, resync, backup and local user recovery commands
- jarl: the CLI, including ACME DNS-01 hooks for certbot and acme.sh
- users with argon2id passwords and CSRF-protected sessions; hashed API
  tokens with granular grants and roles; an audit log
- systemd unit, sysusers.d/tmpfiles.d files, Makefile
- unit tests, integration tests against a real named, scripts/e2e.sh
- docs/ guides, README and CHANGELOG

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-07 15:03:13 +00:00
commit bd6b8778e4
122 files changed
+29217

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
package rr
import (
"sort"
"github.com/miekg/dns"
)
// TypeInfo describes a record type for the web UI and CLI.
type TypeInfo struct {
Type string `json:"type"`
Code uint16 `json:"code"`
Common bool `json:"common"`
Description string `json:"description"`
Format string `json:"format,omitempty"`
Example string `json:"example,omitempty"`
}
var described = []TypeInfo{
{Type: "A", Common: true, Description: "IPv4 address", Format: "<IPv4 address>", Example: "192.0.2.10"},
{Type: "AAAA", Common: true, Description: "IPv6 address", Format: "<IPv6 address>", Example: "2001:db8::10"},
{Type: "CNAME", Common: true, Description: "Alias for another name (no other records may exist at the same name)", Format: "<target name>", Example: "www.example.com."},
{Type: "MX", Common: true, Description: "Mail exchanger", Format: "<preference> <mail server host name>", Example: "10 mail.example.com."},
{Type: "TXT", Common: true, Description: "Text. Plain text is quoted and split into 255-byte strings automatically; start with a quote to give presentation format.", Format: "<text> or \"string\" \"string\"", Example: "v=spf1 mx -all"},
{Type: "NS", Common: true, Description: "Name server for the zone (at the apex) or a delegation (below it)", Format: "<name server host name>", Example: "ns1.example.com."},
{Type: "PTR", Common: true, Description: "Reverse mapping (in in-addr.arpa / ip6.arpa zones)", Format: "<host name>", Example: "host.example.com."},
{Type: "SRV", Common: true, Description: "Service location (owner _service._proto.name)", Format: "<priority> <weight> <port> <target>", Example: "0 5 5060 sip.example.com."},
{Type: "CAA", Common: true, Description: "Certification authority authorization", Format: "<flags> <tag> <value>", Example: "0 issue \"letsencrypt.org\""},
{Type: "HTTPS", Common: true, Description: "HTTPS service binding (RFC 9460)", Format: "<priority> <target> [params...]", Example: "1 . alpn=\"h2,h3\""},
{Type: "SVCB", Description: "General service binding (RFC 9460)", Format: "<priority> <target> [params...]", Example: "1 svc.example.com. alpn=\"h2\" port=8443"},
{Type: "SSHFP", Description: "SSH host key fingerprint", Format: "<algorithm> <fp type> <fingerprint hex>", Example: "4 2 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"},
{Type: "TLSA", Description: "TLS certificate association, DANE (owner _port._proto.name)", Format: "<usage> <selector> <matching type> <data hex>", Example: "3 1 1 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},
{Type: "DS", Description: "Delegation signer for a signed child zone (at a delegation point)", Format: "<key tag> <algorithm> <digest type> <digest hex>", Example: "12345 13 2 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},
{Type: "DNAME", Description: "Redirects a whole subtree to another name", Format: "<target name>", Example: "other.example.net."},
{Type: "NAPTR", Description: "Naming authority pointer", Format: "<order> <preference> \"<flags>\" \"<service>\" \"<regexp>\" <replacement>", Example: "100 10 \"u\" \"E2U+sip\" \"!^.*$!sip:info@example.com!\" ."},
{Type: "LOC", Description: "Geographical location", Format: "<lat> <lon> <alt>m [size [hp [vp]]]", Example: "52 22 23.000 N 4 53 32.000 E -2.00m 0.00m 10000m 10m"},
{Type: "HINFO", Description: "Host information", Format: "\"<cpu>\" \"<os>\"", Example: "\"PC\" \"Linux\""},
{Type: "RP", Description: "Responsible person", Format: "<mailbox name> <TXT record name>", Example: "admin.example.com. ."},
{Type: "URI", Description: "Uniform resource identifier", Format: "<priority> <weight> \"<uri>\"", Example: "10 1 \"https://example.com/\""},
{Type: "OPENPGPKEY", Description: "OpenPGP public key (DANE)", Format: "<base64 key>"},
{Type: "SMIMEA", Description: "S/MIME certificate association", Format: "<usage> <selector> <matching type> <data hex>"},
{Type: "CERT", Description: "Certificate", Format: "<type> <key tag> <algorithm> <base64 certificate>"},
{Type: "DHCID", Description: "DHCP identifier", Format: "<base64 data>"},
{Type: "AFSDB", Description: "AFS database location", Format: "<subtype> <host name>", Example: "1 afs.example.com."},
{Type: "APL", Description: "Address prefix list", Format: "[!]<family>:<prefix>/<len> ...", Example: "1:192.168.32.0/21"},
{Type: "IPSECKEY", Description: "IPsec key", Format: "<precedence> <gateway type> <algorithm> <gateway> <base64 key>"},
{Type: "KX", Description: "Key exchanger", Format: "<preference> <host name>", Example: "10 kx.example.com."},
{Type: "KEY", Description: "Public key (SIG(0) and other legacy uses)", Format: "<flags> <protocol> <algorithm> <base64 key>"},
{Type: "SPF", Description: "Deprecated SPF record type; publish SPF policies as TXT instead", Format: "\"v=spf1 ...\""},
{Type: "CSYNC", Description: "Child-to-parent synchronization", Format: "<soa serial> <flags> <type list>", Example: "66 3 A NS AAAA"},
{Type: "EUI48", Description: "48-bit MAC address", Format: "xx-xx-xx-xx-xx-xx", Example: "00-00-5e-00-53-2a"},
{Type: "EUI64", Description: "64-bit EUI address", Format: "xx-xx-xx-xx-xx-xx-xx-xx", Example: "00-00-5e-ef-10-00-00-2a"},
{Type: "RESINFO", Description: "Resolver information (RFC 9606)", Format: "key=value ...", Example: "qnamemin exterr=15,16,17"},
}
// Types returns information about every record type that can be managed,
// common types first.
func Types() []TypeInfo {
seen := map[string]bool{}
var out []TypeInfo
for _, t := range described {
code, ok := dns.StringToType[t.Type]
if !ok {
continue
}
t.Code = code
out = append(out, t)
seen[t.Type] = true
}
var rest []TypeInfo
for code, name := range dns.TypeToString {
if seen[name] {
continue
}
if _, ok := dns.TypeToRR[code]; !ok {
continue
}
if _, bad := forbidden[code]; bad {
continue
}
rest = append(rest, TypeInfo{Type: name, Code: code, Description: name + " record (zone file format)"})
}
sort.Slice(rest, func(i, j int) bool { return rest[i].Type < rest[j].Type })
return append(out, rest...)
}