aboutsummaryrefslogtreecommitdiff
path: root/bind/builder/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'bind/builder/main.go')
-rw-r--r--bind/builder/main.go586
1 files changed, 586 insertions, 0 deletions
diff --git a/bind/builder/main.go b/bind/builder/main.go
new file mode 100644
index 0000000..aef3754
--- /dev/null
+++ b/bind/builder/main.go
@@ -0,0 +1,586 @@
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "io/ioutil"
7 "log"
8 "os"
9 "os/exec"
10 "os/user"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "syscall"
15 "text/template"
16
17 "gopkg.in/yaml.v2"
18)
19
20var zoneTemplate = template.Must(template.New("bind").Parse(`$ORIGIN .
21$TTL 86400 ; 1 day
22{{ .Zone.Name }} IN SOA {{ .Zone.PrimaryNS }} {{ .Zone.HostmasterEmail }} (
23 1 ; serial
24 604800 ; refresh (1 week)
25 86400 ; retry (1 day)
26 2419200 ; expire (4 weeks)
27 86400 ; minimum (1 day)
28 )
29{{ range .Zone.Nameservers }}
30 NS {{ . }}
31{{ end }}
32`))
33
34var cfgTemplate = template.Must(template.New("bind").Parse(`// vim:ft=named
35{{ range .Keys -}}
36key "{{ .Name }}" {
37 algorithm {{ .Algorithm }};
38 secret "{{ .KeyData }}";
39};
40{{ end }}
41
42{{ range $name, $value := .Acls -}}
43acl {{ $name }} {
44 {{ range $value -}}
45 {{ . }};
46 {{ end }}
47};
48{{ end }}
49
50{{ range .Views -}}
51view {{ .Name }} {
52 match-clients {
53 {{ range .MatchClients -}}
54 {{ . }};
55 {{ end }}
56 };
57
58 {{ .RawInclude }}
59
60 {{- if .Servers }}
61 {{ range .Servers -}}
62 server {{ .Address }} {
63 keys {{ .Key }};
64 };
65 {{- end }}
66 {{ end -}}
67
68 {{ if .NotifySource -}}notify-source {{ .NotifySource }};{{- end }}
69 {{ if .AlsoNotify -}}
70 also-notify {
71 {{- range .AlsoNotify }}
72 {{ .Address }}{{ if .Key }} key {{ .Key }}{{ end }};
73 {{- end }}
74 };
75 {{- end }}
76
77 include "/etc/bind/named.conf.default-zones";
78
79 {{ range .Zones }}
80 zone "{{ .Name }}" {
81 {{ if .InView -}}
82 in-view {{ .InView }};
83 {{- else -}}
84 type {{ .Type }};
85 {{ if .ForwardOnly }}forward only;{{ end }}
86 {{- if .File }}file "{{ .File }}";{{ end }}
87 {{ if .UpdateGrants -}}
88 update-policy {
89 {{- range .UpdateGrants }}
90 grant {{ . }} zonesub ANY;
91 {{- end }}
92 };
93 {{- end }}
94 {{- if .AllowQuery -}}
95 allow-query {
96 {{- range .AllowQuery }}
97 {{ . }};
98 {{- end }}
99 };
100 {{- end -}}
101 {{ if .Forwarders -}}
102 forwarders {
103 {{- range .Forwarders }}
104 {{ . }};
105 {{- end }}
106 };
107 {{- end }}
108 {{ if .Primaries -}}
109 masters {
110 {{- range .Primaries }}
111 {{ . }};
112 {{- end }}
113 };
114 {{- end -}}
115 {{- end }}
116 };
117 {{ end }}
118};
119{{ end }}
120`))
121
122type Server struct {
123 Address string
124 IPs []string `yaml:"ips"`
125 Type string `yaml:"type"`
126 Key *string `yaml:"key"`
127 Forwarders map[string][]string `yaml:"forwarders"`
128}
129
130type ServerMap map[string]*Server
131
132func (l *ServerMap) UnmarshalYAML(unmarshal func(v interface{}) error) error {
133 var rawData map[string]*Server
134 if err := unmarshal(&rawData); err != nil {
135 return err
136 }
137
138 for k, v := range rawData {
139 v.Address = k
140 }
141
142 *l = ServerMap(rawData)
143
144 return nil
145}
146
147type Config struct {
148 Keys map[string]map[string]map[string]string `yaml:"keys"`
149 StaticAcls map[string][]string `yaml:"static-acls"`
150 DynamicAcls map[string]struct {
151 Generator string `yaml:"generator"`
152 Filter string `yaml:"filter"`
153 } `yaml:"dynamic-acls"`
154 Servers ServerMap `yaml:"servers"`
155 Views map[string]struct {
156 MatchClients []string `yaml:"match-clients"`
157 RawInclude string `yaml:"raw-include"`
158 } `yaml:"views"`
159 Zones []struct {
160 Name string `yaml:"name"`
161 Type string `yaml:"type"` // forward-only is only valid type
162 MasterViews []string `yaml:"master-views"`
163 InViews []string `yaml:"in-views"`
164 AllowUpdateKeys []string `yaml:"allow-update-keys"`
165 AllowQuery []string `yaml:"allow-query"`
166 } `yaml:"zones"`
167}
168
169type TemplateData struct {
170 Keys []MaterializedKey
171 Acls map[string][]string
172 Views []MaterializedView
173}
174
175type MaterializedKey struct {
176 Name string
177 Algorithm string
178 KeyData string
179}
180
181type MaterializedView struct {
182 Name string
183 MatchClients []string
184 RawInclude string
185 Zones []MaterializedZone
186 NotifySource *string
187 AlsoNotify []Server
188 Servers []Server
189}
190
191type MaterializedZone struct {
192 Name string
193 Type string
194 File string
195 InView *string
196 ForwardOnly bool
197 UpdateGrants []string
198 Forwarders []string
199 AllowQuery []string
200 Primaries []string
201}
202
203func inSlice(needle string, haystack []string) bool {
204 for _, v := range haystack {
205 if v == needle {
206 return true
207 }
208 }
209 return false
210}
211
212func materializeZones(cfg *Config, forView string, forServer Server) []MaterializedZone {
213 out := []MaterializedZone{}
214
215 for _, zone := range cfg.Zones {
216 var forwarders []string
217 forwardOnly := false
218 zoneType := forServer.Type
219 if zone.Type == "forward-only" {
220 zoneType = "forward"
221 forwardOnly = true
222
223 if fw, ok := forServer.Forwarders[zone.Name]; ok {
224 forwarders = fw
225 } else {
226 // Some forward-only zones don't have forwarders in a given
227 // region, so suppress them if there's nothing to forward to
228 continue
229 }
230 }
231
232 masteredHere := inSlice(forView, zone.MasterViews)
233 exclusiveZone := len(zone.InViews) > 0
234
235 // Zones mastered in a view are always materialized there.
236 //
237 // Zones that specify an in-views clause are only materialized in a
238 // view if that view is listed in the in-views list.
239 //
240 // Zones not mastered in a view and without an in-views list are always
241 // materialized.
242 if masteredHere || !exclusiveZone || (exclusiveZone && inSlice(forView, zone.InViews)) {
243 keys := []string{}
244 primaries := []string{}
245
246 if zoneType == "primary" {
247 // Only primary zones may be updated
248 for _, k := range zone.AllowUpdateKeys {
249 keys = append(keys, fmt.Sprintf("%s-%s", k, forView))
250 }
251 } else if len(forwarders) == 0 { // forward-only zones have forwarders, not primaries
252 // Secondary zones use all primary servers as their primaries
253 for _, v := range cfg.Servers {
254 if v.Type == "primary" {
255 primaries = append(primaries, v.Address)
256 }
257 }
258 }
259
260 // Zones not mastered here are considered to be mastered in their
261 // first master view. This may not be correct for views nested in
262 // views.
263 var masterView *string
264 if !masteredHere {
265 masterView = &zone.MasterViews[0]
266 }
267
268 // forward-only zones are memory resident and have no file
269 fileName := fmt.Sprintf("%s/db.%s", forView, zone.Name)
270 if zoneType == "forward" {
271 fileName = ""
272 }
273
274 out = append(out, MaterializedZone{
275 Name: zone.Name,
276 Type: zoneType,
277 File: fileName,
278 InView: masterView,
279 ForwardOnly: forwardOnly,
280 UpdateGrants: keys,
281 Forwarders: forwarders,
282 AllowQuery: zone.AllowQuery,
283 Primaries: primaries,
284 })
285 }
286 }
287
288 return out
289}
290
291func getSecondaryServers(cfg *Config, viewName string) []Server {
292 out := []Server{}
293 for _, v := range cfg.Servers {
294 if v.Type == "secondary" {
295 kv := fmt.Sprintf("%s-%s", *v.Key, viewName)
296 out = append(out, Server{
297 Address: v.Address,
298 Key: &kv,
299 })
300 }
301 }
302 return out
303}
304
305func materializeViews(cfg *Config, forServer Server) []MaterializedView {
306 out := []MaterializedView{}
307
308 for viewName, configView := range cfg.Views {
309 var notifySource *string
310 var servers []Server
311 var alsoNotify []Server
312
313 if forServer.Type == "primary" {
314 // Only primary zones do notifies
315 notifySource = &forServer.Address
316 alsoNotify = getSecondaryServers(cfg, viewName)
317 } else {
318 myKey := fmt.Sprintf("%s-%s", *forServer.Key, viewName)
319
320 for _, s := range cfg.Servers {
321 if s.Type == "primary" {
322 servers = append(servers, Server{
323 Address: s.Address,
324 Key: &myKey,
325 })
326 }
327 }
328 }
329
330 out = append(out, MaterializedView{
331 Name: viewName,
332 MatchClients: configView.MatchClients,
333 RawInclude: configView.RawInclude,
334 Zones: materializeZones(cfg, viewName, forServer),
335 AlsoNotify: alsoNotify,
336 NotifySource: notifySource,
337 Servers: servers,
338 })
339 }
340 return out
341}
342
343func materializeKeys(cfg *Config) []MaterializedKey {
344 out := []MaterializedKey{}
345 // Flatten the key YAML structure to <key_name>-<view>
346 for name, subKeys := range cfg.Keys {
347 for view, key := range subKeys {
348 for keyAlgo, keyData := range key {
349 out = append(out, MaterializedKey{
350 Name: fmt.Sprintf("%s-%s", name, view),
351 Algorithm: keyAlgo,
352 KeyData: keyData,
353 })
354 }
355 }
356 }
357 return out
358}
359
360func aclGenerateServers(cfg *Config) []string {
361 lines := []string{}
362
363 for _, server := range cfg.Servers {
364 lines = append(lines, server.Address)
365 lines = append(lines, server.IPs...)
366 }
367
368 return lines
369}
370
371// Filters use the https://golang.org/pkg/path/filepath/#Match syntax. If no
372// filter is specified then the default is "*".
373func aclGenerateKeys(cfg *Config, keys []MaterializedKey, filter string) []string {
374 lines := []string{}
375
376 if filter == "" {
377 filter = "*"
378 }
379
380 for _, k := range keys {
381 if ok, _ := filepath.Match(filter, k.Name); ok {
382 lines = append(lines, fmt.Sprintf("key %s", k.Name))
383 }
384 }
385
386 return lines
387}
388
389func materializeAcls(cfg *Config, keys []MaterializedKey) map[string][]string {
390 out := map[string][]string{}
391
392 keyIndex := map[string]bool{}
393 for _, k := range keys {
394 keyIndex[k.Name] = true
395 }
396
397 // Generate dynamic ACLs
398 for aclName, acl := range cfg.DynamicAcls {
399 switch acl.Generator {
400 case "servers":
401 out[aclName] = aclGenerateServers(cfg)
402 case "keys":
403 out[aclName] = aclGenerateKeys(cfg, keys, acl.Filter)
404 }
405 }
406
407 // For static key-based ACLs only emit key lines for keys we have
408 //
409 // TODO: This was for an old iteration of the config format. All key-based
410 // ACLs should be generated now. If that's the case then this loop can just
411 // be removed.
412 for aclName, acl := range cfg.StaticAcls {
413 validLines := []string{}
414 for _, line := range acl {
415 parts := strings.Split(line, " ")
416 if parts[0] == "key" {
417 if _, ok := keyIndex[parts[1]]; !ok {
418 continue
419 }
420 }
421
422 validLines = append(validLines, line)
423 }
424 out[aclName] = validLines
425 }
426
427 return out
428}
429
430func loadYaml(filename string, into interface{}) error {
431 rawData, err := ioutil.ReadFile(filename)
432 if err != nil {
433 return err
434 }
435
436 if err = yaml.Unmarshal(rawData, into); err != nil {
437 return err
438 }
439
440 return nil
441}
442
443func lookupUserGroup(un, gn string) (int, int, error) {
444 u, err := user.Lookup(un)
445 if err != nil {
446 return 0, 0, err
447 }
448
449 g, err := user.LookupGroup(gn)
450 if err != nil {
451 return 0, 0, nil
452 }
453
454 uid, err := strconv.Atoi(u.Uid)
455 if err != nil {
456 return 0, 0, err
457 }
458
459 gid, err := strconv.Atoi(g.Gid)
460 if err != nil {
461 return 0, 0, err
462 }
463
464 return uid, gid, nil
465}
466
467func generateNamedConf(forServer string, cfgFile, keysFile string) []byte {
468 var out bytes.Buffer
469
470 cfg := Config{}
471 if err := loadYaml(cfgFile, &cfg); err != nil {
472 log.Fatalf("error: %s", err)
473 }
474
475 if err := loadYaml(keysFile, &cfg); err != nil {
476 log.Fatalf("error: %s", err)
477 }
478
479 server, ok := cfg.Servers[forServer]
480 if !ok {
481 log.Fatalf("No server %s\n", forServer)
482 }
483
484 keys := materializeKeys(&cfg)
485
486 cfgTemplate.Execute(&out, &TemplateData{
487 Keys: keys,
488 Acls: materializeAcls(&cfg, keys),
489 Views: materializeViews(&cfg, *server),
490 })
491
492 return out.Bytes()
493}
494
495// This will replace the current process
496func execToProcess(args []string) {
497 log.Printf("running: %s %+v", args[1], args[1:])
498 if err := syscall.Exec(args[1], args[1:], []string{}); err != nil {
499 log.Fatalf("error: exec failed %s", err)
500 }
501}
502
503func writeFile(path string, contents []byte, mode os.FileMode, uid, gid int) error {
504 if err := ioutil.WriteFile(path, contents, mode); err != nil {
505 return err
506 }
507
508 if err := os.Chown(path, uid, gid); err != nil {
509 return err
510 }
511
512 return nil
513}
514
515// This is used for RNDC and only from the current host, so just generate
516// it fresh each container start.
517func generateRNDCKey(path string, uid, gid int) error {
518 var out bytes.Buffer
519 cmd := exec.Command("ddns-confgen", "-q", "-k", "rndc-key")
520 cmd.Stdout = &out
521 if err := cmd.Run(); err != nil {
522 return err
523 }
524
525 if err := writeFile(path, out.Bytes(), 0440, uid, gid); err != nil {
526 return err
527 }
528
529 return nil
530}
531
532func ensureDirExists(path string, mode os.FileMode, uid, gid int) {
533 _ = os.Mkdir(path, mode)
534 os.Chown(path, uid, gid)
535}
536
537func realMain() {
538 forServer := os.Getenv("SERVER_ADDRESS")
539 if forServer == "" {
540 log.Fatalf("error: SERVER_ADDRESS is not in environment")
541 }
542
543 namedUser, namedGroup, err := lookupUserGroup("named", "named")
544 if err != nil {
545 log.Fatalf("error: %s", err)
546 }
547
548 cfg := generateNamedConf(forServer, "/etc/bind/zones.yaml", "keys.yaml")
549 if err := writeFile("/etc/bind/named_local.conf", cfg, 0644, namedUser, namedGroup); err != nil {
550 log.Fatalf("error: %s", err)
551 }
552
553 // Ensure these directories exist
554 ensureDirExists("/etc/bind/local/cache", 0755, namedUser, namedGroup)
555 ensureDirExists("/etc/bind/local/cache/internal", 0755, namedUser, namedGroup)
556 ensureDirExists("/etc/bind/local/cache/external", 0755, namedUser, namedGroup)
557
558 // TODO: Write missing zone files for master
559
560 if err := generateRNDCKey("/etc/bind/rndc.key", namedUser, namedGroup); err != nil {
561 log.Fatalf("error: %s", err)
562 }
563
564 execToProcess(os.Args)
565}
566
567func testMain() {
568 forServer := os.Getenv("SERVER_ADDRESS")
569 if forServer == "" {
570 log.Fatalf("error: SERVER_ADDRESS is not in environment")
571 }
572
573 namedUser, namedGroup, err := lookupUserGroup("mcrute", "mcrute")
574 if err != nil {
575 log.Fatalf("error: %s", err)
576 }
577
578 cfg := generateNamedConf(forServer, "zones.yaml", "keys.yaml")
579 if err := writeFile("named_local.conf", cfg, 0644, namedUser, namedGroup); err != nil {
580 log.Fatalf("error: %s", err)
581 }
582}
583
584func main() {
585 testMain()
586}