blob: 05446112ab49084593d5e9c67f2cee44a2f7a11a [file] [log] [blame]
gioe72b54f2024-04-22 10:44:41 +04001package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7 "text/template"
8 "time"
9
10 "github.com/Masterminds/sprig/v3"
11 "github.com/miekg/dns"
12)
13
14const coreDNSConfigTmpl = `
15{{ .zone }}:53 {
16 file {{ .dbFile }} {
17 reload 1s
18 }
19 errors
20 {{- if .dnsSecBasename }}
21 dnssec {
22 key file {{ .dnsSecBasename}}
23 }
24 {{- end }}
25 log
26 health {
27 lameduck 5s
28 }
29 ready
30 cache 30
31 loop
32 reload
33 loadbalance
34}
35`
36
37const recordsDBTmpl = `
38{{- $zone := .zone }}
39{{ $zone }}. IN SOA ns1.{{ $zone }}. hostmaster.{{ $zone }}. {{ .nowUnix }} 7200 3600 1209600 3600
40{{- range $i, $ns := .nameserverIP }}
41ns{{ add1 $i }}.{{ $zone }}. 10800 IN A {{ $ns }}
42{{- end }}
43{{- range .publicIP }}
44{{ $zone }}. 10800 IN A {{ . }}
45*.{{ $zone }}. 10800 IN A {{ . }}
46*.*.{{ $zone }}. 10800 IN A {{ . }}
47{{- end }}
48*.p.{{ $zone }}. 10800 IN A {{ .privateIP }}
49`
50
51func NewStore(fs FS, config string, db string, zone string, publicIP []string, privateIP string, nameserverIP []string) (RecordStore, string, error) {
52 dnsSec, err := getDNSSecKey(fs, zone)
53 if err != nil {
54 return nil, "", err
55 }
56 if err := fs.Write(dnsSec.Basename+".key", string(dnsSec.Key)); err != nil {
57 return nil, "", err
58 }
59 if err := fs.Write(dnsSec.Basename+".private", string(dnsSec.Private)); err != nil {
60 return nil, "", err
61 }
62 if err := executeTemplate(fs, config, coreDNSConfigTmpl, map[string]any{
63 "zone": zone,
64 "dbFile": fs.AbsolutePath(db),
65 "dnsSecBasename": fs.AbsolutePath(dnsSec.Basename),
66 }); err != nil {
67 return nil, "", err
68 }
69 ok, err := fs.Exists(db)
70 if err != nil {
71 return nil, "", err
72 }
73 if !ok {
74 if err := executeTemplate(fs, db, recordsDBTmpl, map[string]any{
75 "zone": zone,
76 "publicIP": publicIP,
77 "privateIP": privateIP,
78 "nameserverIP": nameserverIP,
79 "nowUnix": NowUnix(),
80 }); err != nil {
81 return nil, "", err
82 }
83 }
84 return &fsRecordStore{zone, publicIP, fs, db}, string(dnsSec.DS), nil
85}
86
87func getDNSSecKey(fs FS, zone string) (DNSSecKey, error) {
88 const configFile = "dns-sec-key.json"
89 ok, err := fs.Exists(configFile)
90 if err != nil {
91 return DNSSecKey{}, err
92 }
93 if ok {
94 d, err := fs.Read(configFile)
95 if err != nil {
96 return DNSSecKey{}, err
97 }
98 var k DNSSecKey
99 if err := json.Unmarshal([]byte(d), &k); err != nil {
100 return DNSSecKey{}, err
101 }
102 return k, nil
103 }
104 k, err := newDNSSecKey(zone)
105 if err != nil {
106 return DNSSecKey{}, err
107 }
108 d, err := json.MarshalIndent(k, "", "\t")
109 if err != nil {
110 return DNSSecKey{}, err
111 }
112 if err := fs.Write(configFile, string(d)); err != nil {
113 return DNSSecKey{}, err
114 }
115 return k, nil
116}
117
118type DNSSecKey struct {
119 Basename string `json:"basename,omitempty"`
120 Key []byte `json:"key,omitempty"`
121 Private []byte `json:"private,omitempty"`
122 DS []byte `json:"ds,omitempty"`
123}
124
125func newDNSSecKey(zone string) (DNSSecKey, error) {
126 key := &dns.DNSKEY{
127 Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
128 Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
129 }
130 priv, err := key.Generate(256)
131 if err != nil {
132 return DNSSecKey{}, err
133 }
134 return DNSSecKey{
135 Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
136 Key: []byte(key.String()),
137 Private: []byte(key.PrivateKeyString(priv)),
138 DS: []byte(key.ToDS(dns.SHA256).String()),
139 }, nil
140}
141
142// TODO(gio): not going to work in 15 years?
143// TODO(gio): remove 10 *
144func NowUnix() uint32 {
145 return 10 * uint32(time.Now().Unix())
146}
147
148func executeTemplate(fs FS, path string, contents string, values map[string]any) error {
149 tmpl, err := template.New("tmpl").Funcs(sprig.TxtFuncMap()).Parse(contents)
150 if err != nil {
151 return err
152 }
153 var d strings.Builder
154 if err := tmpl.Execute(&d, values); err != nil {
155 return err
156 }
157 return fs.Write(path, strings.TrimSpace(d.String())+"\n")
158}