blob: 5e494ae219b9e8d292ede1e07a4eb7f77d21fd97 [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 }}
gio721c0042025-04-03 11:56:36 +040043ns.p.{{ $zone }}. 10800 IN A 100.100.100.100
44devices.p.{{ $zone }}. 10800 IN NS ns.p.{{ $zone }}
gioe72b54f2024-04-22 10:44:41 +040045{{- range .publicIP }}
46{{ $zone }}. 10800 IN A {{ . }}
47*.{{ $zone }}. 10800 IN A {{ . }}
48*.*.{{ $zone }}. 10800 IN A {{ . }}
49{{- end }}
50*.p.{{ $zone }}. 10800 IN A {{ .privateIP }}
51`
52
53func NewStore(fs FS, config string, db string, zone string, publicIP []string, privateIP string, nameserverIP []string) (RecordStore, string, error) {
54 dnsSec, err := getDNSSecKey(fs, zone)
55 if err != nil {
56 return nil, "", err
57 }
58 if err := fs.Write(dnsSec.Basename+".key", string(dnsSec.Key)); err != nil {
59 return nil, "", err
60 }
61 if err := fs.Write(dnsSec.Basename+".private", string(dnsSec.Private)); err != nil {
62 return nil, "", err
63 }
64 if err := executeTemplate(fs, config, coreDNSConfigTmpl, map[string]any{
65 "zone": zone,
66 "dbFile": fs.AbsolutePath(db),
67 "dnsSecBasename": fs.AbsolutePath(dnsSec.Basename),
68 }); err != nil {
69 return nil, "", err
70 }
71 ok, err := fs.Exists(db)
72 if err != nil {
73 return nil, "", err
74 }
75 if !ok {
76 if err := executeTemplate(fs, db, recordsDBTmpl, map[string]any{
77 "zone": zone,
78 "publicIP": publicIP,
79 "privateIP": privateIP,
80 "nameserverIP": nameserverIP,
81 "nowUnix": NowUnix(),
82 }); err != nil {
83 return nil, "", err
84 }
85 }
86 return &fsRecordStore{zone, publicIP, fs, db}, string(dnsSec.DS), nil
87}
88
89func getDNSSecKey(fs FS, zone string) (DNSSecKey, error) {
90 const configFile = "dns-sec-key.json"
91 ok, err := fs.Exists(configFile)
92 if err != nil {
93 return DNSSecKey{}, err
94 }
95 if ok {
96 d, err := fs.Read(configFile)
97 if err != nil {
98 return DNSSecKey{}, err
99 }
100 var k DNSSecKey
101 if err := json.Unmarshal([]byte(d), &k); err != nil {
102 return DNSSecKey{}, err
103 }
104 return k, nil
105 }
106 k, err := newDNSSecKey(zone)
107 if err != nil {
108 return DNSSecKey{}, err
109 }
110 d, err := json.MarshalIndent(k, "", "\t")
111 if err != nil {
112 return DNSSecKey{}, err
113 }
114 if err := fs.Write(configFile, string(d)); err != nil {
115 return DNSSecKey{}, err
116 }
117 return k, nil
118}
119
120type DNSSecKey struct {
121 Basename string `json:"basename,omitempty"`
122 Key []byte `json:"key,omitempty"`
123 Private []byte `json:"private,omitempty"`
124 DS []byte `json:"ds,omitempty"`
125}
126
127func newDNSSecKey(zone string) (DNSSecKey, error) {
128 key := &dns.DNSKEY{
129 Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
130 Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
131 }
132 priv, err := key.Generate(256)
133 if err != nil {
134 return DNSSecKey{}, err
135 }
136 return DNSSecKey{
137 Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
138 Key: []byte(key.String()),
139 Private: []byte(key.PrivateKeyString(priv)),
140 DS: []byte(key.ToDS(dns.SHA256).String()),
141 }, nil
142}
143
144// TODO(gio): not going to work in 15 years?
145// TODO(gio): remove 10 *
146func NowUnix() uint32 {
147 return 10 * uint32(time.Now().Unix())
148}
149
150func executeTemplate(fs FS, path string, contents string, values map[string]any) error {
151 tmpl, err := template.New("tmpl").Funcs(sprig.TxtFuncMap()).Parse(contents)
152 if err != nil {
153 return err
154 }
155 var d strings.Builder
156 if err := tmpl.Execute(&d, values); err != nil {
157 return err
158 }
159 return fs.Write(path, strings.TrimSpace(d.String())+"\n")
160}