package bind import ( "fmt" "sort" "strings" bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1" ) // RenderInput aggregates everything needed to render a cluster's named.conf. type RenderInput struct { Cluster *bindv1alpha1.BindCluster ACLs []bindv1alpha1.BindACL Views []bindv1alpha1.BindView Policies []bindv1alpha1.BindPolicy DNSSECPolicies []bindv1alpha1.BindDNSSECPolicy Catalog *bindv1alpha1.BindCatalogZone // PrimaryAddress is the in-cluster address secondaries transfer from. PrimaryAddress string } // RenderNamedConf returns the primary and secondary named.conf contents for a // cluster. Both variants are shipped in the ConfigMap; the entrypoint selects // one based on the pod ordinal. func RenderNamedConf(in RenderInput) (primary string, secondary string) { return render(in, true), render(in, false) } func render(in RenderInput, isPrimary bool) string { c := in.Cluster var b strings.Builder b.WriteString("// Managed by bind-operator. Do not edit.\n") b.WriteString(fmt.Sprintf("include \"%s\";\n", RndcKeyPath)) b.WriteString(fmt.Sprintf("include \"%s\";\n\n", KeysConfPath)) // Named ACLs (global scope). acls := append([]bindv1alpha1.BindACL(nil), in.ACLs...) sort.Slice(acls, func(i, j int) bool { return acls[i].Name < acls[j].Name }) for _, a := range acls { b.WriteString(fmt.Sprintf("acl \"%s\" { %s };\n", a.Name, matchList(a.Spec.Entries))) } if len(acls) > 0 { b.WriteString("\n") } // DNSSEC policies (must precede zones that reference them). for _, p := range in.DNSSECPolicies { b.WriteString(renderDNSSECPolicy(p)) } // options. b.WriteString("options {\n") b.WriteString(fmt.Sprintf(" directory \"%s\";\n", DataDir)) b.WriteString(" listen-on port 53 { any; };\n") b.WriteString(" listen-on-v6 port 53 { any; };\n") b.WriteString(fmt.Sprintf(" recursion %s;\n", yesno(recursionFor(c)))) if len(c.Spec.Forwarders) > 0 { b.WriteString(fmt.Sprintf(" forwarders { %s };\n", terminate(c.Spec.Forwarders))) } if allowNewZones(c) { b.WriteString(" allow-new-zones yes;\n") } b.WriteString(" dnssec-validation auto;\n") for _, o := range c.Spec.ExtraOptions { b.WriteString(" " + strings.TrimRight(o, ";") + ";\n") } // When there are no views, response-policy and catalog-zones live in options. if len(in.Views) == 0 { b.WriteString(responsePolicyClause(in.Policies, " ")) b.WriteString(catalogZonesClause(in, isPrimary, " ")) } b.WriteString("};\n\n") // controls (rndc). b.WriteString("controls {\n") b.WriteString(" inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { \"rndc-key\"; };\n") b.WriteString("};\n\n") // Views, if any. views := append([]bindv1alpha1.BindView(nil), in.Views...) sort.Slice(views, func(i, j int) bool { return views[i].Spec.Order < views[j].Spec.Order }) for _, v := range views { b.WriteString(renderView(v, in, isPrimary)) } // Catalog zone declaration lives at top level when there are no views. if in.Catalog != nil && len(in.Views) == 0 { b.WriteString(renderCatalogZoneDecl(in, isPrimary, "")) } return b.String() } func renderView(v bindv1alpha1.BindView, in RenderInput, isPrimary bool) string { var b strings.Builder b.WriteString(fmt.Sprintf("view \"%s\" {\n", v.Name)) mc := v.Spec.MatchClients if len(mc) == 0 { mc = []string{"any"} } b.WriteString(fmt.Sprintf(" match-clients { %s };\n", matchList(mc))) if len(v.Spec.MatchDestinations) > 0 { b.WriteString(fmt.Sprintf(" match-destinations { %s };\n", matchList(v.Spec.MatchDestinations))) } rec := recursionFor(in.Cluster) if v.Spec.Recursion != nil { rec = *v.Spec.Recursion } b.WriteString(fmt.Sprintf(" recursion %s;\n", yesno(rec))) if len(v.Spec.AllowQuery) > 0 { b.WriteString(fmt.Sprintf(" allow-query { %s };\n", matchList(v.Spec.AllowQuery))) } for _, o := range v.Spec.ExtraOptions { b.WriteString(" " + strings.TrimRight(o, ";") + ";\n") } // Policies and catalog scoped to this view. viewPolicies := filterPoliciesForView(in.Policies, v.Name) b.WriteString(responsePolicyClause(viewPolicies, " ")) b.WriteString(catalogZonesClause(in, isPrimary, " ")) if in.Catalog != nil { b.WriteString(renderCatalogZoneDecl(in, isPrimary, " ")) } b.WriteString("};\n\n") return b.String() } func renderDNSSECPolicy(p bindv1alpha1.BindDNSSECPolicy) string { name := p.Spec.PolicyName if name == "" { name = p.Name } var b strings.Builder b.WriteString(fmt.Sprintf("dnssec-policy \"%s\" {\n", name)) if p.Spec.NSEC3 { b.WriteString(" nsec3param;\n") } if p.Spec.MaxZoneTTL != "" { b.WriteString(fmt.Sprintf(" max-zone-ttl %s;\n", p.Spec.MaxZoneTTL)) } if p.Spec.SignaturesValidity != "" { b.WriteString(fmt.Sprintf(" signatures-validity %s;\n", p.Spec.SignaturesValidity)) } alg := p.Spec.Algorithm if alg == "" { alg = "ecdsap256sha256" } if p.Spec.CSK != nil { b.WriteString(" keys {\n") b.WriteString(" csk " + keyLine(p.Spec.CSK, alg) + ";\n") b.WriteString(" };\n") } else { b.WriteString(" keys {\n") if p.Spec.KSK != nil { b.WriteString(" ksk " + keyLine(p.Spec.KSK, alg) + ";\n") } if p.Spec.ZSK != nil { b.WriteString(" zsk " + keyLine(p.Spec.ZSK, alg) + ";\n") } b.WriteString(" };\n") } for _, o := range p.Spec.ExtraOptions { b.WriteString(" " + strings.TrimRight(o, ";") + ";\n") } b.WriteString("};\n\n") return b.String() } func keyLine(k *bindv1alpha1.DNSSECKey, defaultAlg string) string { lifetime := k.Lifetime if lifetime == "" { lifetime = "unlimited" } alg := k.Algorithm if alg == "" { alg = defaultAlg } if k.KeySize > 0 { return fmt.Sprintf("lifetime %s algorithm %s %d", lifetime, alg, k.KeySize) } return fmt.Sprintf("lifetime %s algorithm %s", lifetime, alg) } func responsePolicyClause(policies []bindv1alpha1.BindPolicy, indent string) string { if len(policies) == 0 { return "" } sorted := append([]bindv1alpha1.BindPolicy(nil), policies...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Spec.Order < sorted[j].Spec.Order }) var b strings.Builder b.WriteString(indent + "response-policy {\n") for _, p := range sorted { b.WriteString(fmt.Sprintf("%s zone \"%s\";\n", indent, p.Spec.ZoneName)) } b.WriteString(indent + "};\n") return b.String() } func catalogZonesClause(in RenderInput, isPrimary bool, indent string) string { // Only secondaries consume the catalog to auto-provision member zones. if in.Catalog == nil || isPrimary { return "" } primaries := in.Catalog.Spec.DefaultPrimaries if len(primaries) == 0 && in.PrimaryAddress != "" { primaries = []string{in.PrimaryAddress} } var b strings.Builder b.WriteString(indent + "catalog-zones {\n") b.WriteString(fmt.Sprintf("%s zone \"%s\" default-primaries { %s };\n", indent, in.Catalog.Spec.ZoneName, terminate(primaries))) b.WriteString(indent + "};\n") return b.String() } // renderCatalogZoneDecl declares the catalog zone as a secondary on consumer // pods. The primary hosts the catalog zone dynamically (created by the // BindCatalogZone controller via rndc addzone), so nothing is emitted here for // the primary. func renderCatalogZoneDecl(in RenderInput, isPrimary bool, indent string) string { if isPrimary { return "" } cat := in.Catalog file := CatalogFilePath(cat.Spec.ZoneName) primaries := cat.Spec.DefaultPrimaries if len(primaries) == 0 && in.PrimaryAddress != "" { primaries = []string{in.PrimaryAddress} } var b strings.Builder b.WriteString(fmt.Sprintf("%szone \"%s\" {\n", indent, cat.Spec.ZoneName)) b.WriteString(indent + " type secondary;\n") b.WriteString(fmt.Sprintf("%s file \"%s\";\n", indent, file)) b.WriteString(fmt.Sprintf("%s primaries { %s };\n", indent, terminate(primaries))) b.WriteString(indent + "};\n\n") return b.String() } func filterPoliciesForView(policies []bindv1alpha1.BindPolicy, view string) []bindv1alpha1.BindPolicy { var out []bindv1alpha1.BindPolicy for _, p := range policies { if p.Spec.ViewRef == view || p.Spec.ViewRef == "" { out = append(out, p) } } return out } // matchList renders address-match-list elements, each terminated with a // semicolon: `10.0.0.0/8; key foo;`. func matchList(entries []string) string { return terminate(entries) } // terminate joins elements each followed by "; ". func terminate(entries []string) string { var parts []string for _, e := range entries { e = strings.TrimSpace(strings.TrimRight(e, ";")) if e == "" { continue } parts = append(parts, e+";") } return strings.Join(parts, " ") } func yesno(b bool) string { if b { return "yes" } return "no" } func recursionFor(c *bindv1alpha1.BindCluster) bool { if c.Spec.Recursion != nil { return *c.Spec.Recursion } return c.Spec.Mode == bindv1alpha1.ModeResolver } func allowNewZones(c *bindv1alpha1.BindCluster) bool { if c.Spec.AllowNewZones != nil { return *c.Spec.AllowNewZones } return true }