blob: 05446112ab49084593d5e9c67f2cee44a2f7a11a [file] [log] [blame]
package main
import (
"encoding/json"
"fmt"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/miekg/dns"
)
const coreDNSConfigTmpl = `
{{ .zone }}:53 {
file {{ .dbFile }} {
reload 1s
}
errors
{{- if .dnsSecBasename }}
dnssec {
key file {{ .dnsSecBasename}}
}
{{- end }}
log
health {
lameduck 5s
}
ready
cache 30
loop
reload
loadbalance
}
`
const recordsDBTmpl = `
{{- $zone := .zone }}
{{ $zone }}. IN SOA ns1.{{ $zone }}. hostmaster.{{ $zone }}. {{ .nowUnix }} 7200 3600 1209600 3600
{{- range $i, $ns := .nameserverIP }}
ns{{ add1 $i }}.{{ $zone }}. 10800 IN A {{ $ns }}
{{- end }}
{{- range .publicIP }}
{{ $zone }}. 10800 IN A {{ . }}
*.{{ $zone }}. 10800 IN A {{ . }}
*.*.{{ $zone }}. 10800 IN A {{ . }}
{{- end }}
*.p.{{ $zone }}. 10800 IN A {{ .privateIP }}
`
func NewStore(fs FS, config string, db string, zone string, publicIP []string, privateIP string, nameserverIP []string) (RecordStore, string, error) {
dnsSec, err := getDNSSecKey(fs, zone)
if err != nil {
return nil, "", err
}
if err := fs.Write(dnsSec.Basename+".key", string(dnsSec.Key)); err != nil {
return nil, "", err
}
if err := fs.Write(dnsSec.Basename+".private", string(dnsSec.Private)); err != nil {
return nil, "", err
}
if err := executeTemplate(fs, config, coreDNSConfigTmpl, map[string]any{
"zone": zone,
"dbFile": fs.AbsolutePath(db),
"dnsSecBasename": fs.AbsolutePath(dnsSec.Basename),
}); err != nil {
return nil, "", err
}
ok, err := fs.Exists(db)
if err != nil {
return nil, "", err
}
if !ok {
if err := executeTemplate(fs, db, recordsDBTmpl, map[string]any{
"zone": zone,
"publicIP": publicIP,
"privateIP": privateIP,
"nameserverIP": nameserverIP,
"nowUnix": NowUnix(),
}); err != nil {
return nil, "", err
}
}
return &fsRecordStore{zone, publicIP, fs, db}, string(dnsSec.DS), nil
}
func getDNSSecKey(fs FS, zone string) (DNSSecKey, error) {
const configFile = "dns-sec-key.json"
ok, err := fs.Exists(configFile)
if err != nil {
return DNSSecKey{}, err
}
if ok {
d, err := fs.Read(configFile)
if err != nil {
return DNSSecKey{}, err
}
var k DNSSecKey
if err := json.Unmarshal([]byte(d), &k); err != nil {
return DNSSecKey{}, err
}
return k, nil
}
k, err := newDNSSecKey(zone)
if err != nil {
return DNSSecKey{}, err
}
d, err := json.MarshalIndent(k, "", "\t")
if err != nil {
return DNSSecKey{}, err
}
if err := fs.Write(configFile, string(d)); err != nil {
return DNSSecKey{}, err
}
return k, nil
}
type DNSSecKey struct {
Basename string `json:"basename,omitempty"`
Key []byte `json:"key,omitempty"`
Private []byte `json:"private,omitempty"`
DS []byte `json:"ds,omitempty"`
}
func newDNSSecKey(zone string) (DNSSecKey, error) {
key := &dns.DNSKEY{
Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
}
priv, err := key.Generate(256)
if err != nil {
return DNSSecKey{}, err
}
return DNSSecKey{
Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
Key: []byte(key.String()),
Private: []byte(key.PrivateKeyString(priv)),
DS: []byte(key.ToDS(dns.SHA256).String()),
}, nil
}
// TODO(gio): not going to work in 15 years?
// TODO(gio): remove 10 *
func NowUnix() uint32 {
return 10 * uint32(time.Now().Unix())
}
func executeTemplate(fs FS, path string, contents string, values map[string]any) error {
tmpl, err := template.New("tmpl").Funcs(sprig.TxtFuncMap()).Parse(contents)
if err != nil {
return err
}
var d strings.Builder
if err := tmpl.Execute(&d, values); err != nil {
return err
}
return fs.Write(path, strings.TrimSpace(d.String())+"\n")
}