github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/bind/bindProvider.go (about) 1 package bind 2 3 /* 4 5 bind - 6 Generate zonefiles suitable for BIND. 7 8 The zonefiles are read and written to the directory -bind_dir 9 10 If the old zonefiles are readable, we read them to determine 11 if an update is actually needed. The old zonefile is also used 12 as the basis for generating the new SOA serial number. 13 14 */ 15 16 import ( 17 "bytes" 18 "encoding/json" 19 "fmt" 20 "io/ioutil" 21 "os" 22 "path/filepath" 23 "strings" 24 "time" 25 26 "github.com/miekg/dns" 27 28 "github.com/StackExchange/dnscontrol/v2/models" 29 "github.com/StackExchange/dnscontrol/v2/pkg/prettyzone" 30 "github.com/StackExchange/dnscontrol/v2/providers" 31 "github.com/StackExchange/dnscontrol/v2/providers/diff" 32 ) 33 34 var features = providers.DocumentationNotes{ 35 providers.CanUseCAA: providers.Can(), 36 providers.CanUsePTR: providers.Can(), 37 providers.CanUseNAPTR: providers.Can(), 38 providers.CanUseSRV: providers.Can(), 39 providers.CanUseSSHFP: providers.Can(), 40 providers.CanUseTLSA: providers.Can(), 41 providers.CanUseTXTMulti: providers.Can(), 42 providers.CanAutoDNSSEC: providers.Can("Just writes out a comment indicating DNSSEC was requested"), 43 providers.CantUseNOPURGE: providers.Cannot(), 44 providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."), 45 providers.DocDualHost: providers.Can(), 46 providers.DocOfficiallySupported: providers.Can(), 47 providers.CanGetZones: providers.Can(), 48 } 49 50 func initBind(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) { 51 // config -- the key/values from creds.json 52 // meta -- the json blob from NewReq('name', 'TYPE', meta) 53 api := &Bind{ 54 directory: config["directory"], 55 } 56 if api.directory == "" { 57 api.directory = "zones" 58 } 59 if len(providermeta) != 0 { 60 err := json.Unmarshal(providermeta, api) 61 if err != nil { 62 return nil, err 63 } 64 } 65 api.nameservers = models.StringsToNameservers(api.DefaultNS) 66 return api, nil 67 } 68 69 func init() { 70 providers.RegisterDomainServiceProviderType("BIND", initBind, features) 71 } 72 73 // SoaInfo contains the parts of the default SOA settings. 74 type SoaInfo struct { 75 Ns string `json:"master"` 76 Mbox string `json:"mbox"` 77 Serial uint32 `json:"serial"` 78 Refresh uint32 `json:"refresh"` 79 Retry uint32 `json:"retry"` 80 Expire uint32 `json:"expire"` 81 Minttl uint32 `json:"minttl"` 82 } 83 84 func (s SoaInfo) String() string { 85 return fmt.Sprintf("%s %s %d %d %d %d %d", s.Ns, s.Mbox, s.Serial, s.Refresh, s.Retry, s.Expire, s.Minttl) 86 } 87 88 // Bind is the provider handle for the Bind driver. 89 type Bind struct { 90 DefaultNS []string `json:"default_ns"` 91 DefaultSoa SoaInfo `json:"default_soa"` 92 nameservers []*models.Nameserver 93 directory string 94 zonefile string // Where the zone data is expected 95 zoneFileFound bool // Did the zonefile exist? 96 } 97 98 // GetNameservers returns the nameservers for a domain. 99 func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) { 100 return c.nameservers, nil 101 } 102 103 // ListZones returns all the zones in an account 104 func (c *Bind) ListZones() ([]string, error) { 105 if _, err := os.Stat(c.directory); os.IsNotExist(err) { 106 return nil, fmt.Errorf("directory %q does not exist", c.directory) 107 } 108 109 filenames, err := filepath.Glob(filepath.Join(c.directory, "*.zone")) 110 if err != nil { 111 return nil, err 112 } 113 var zones []string 114 for _, n := range filenames { 115 _, file := filepath.Split(n) 116 zones = append(zones, strings.TrimSuffix(file, ".zone")) 117 } 118 return zones, nil 119 } 120 121 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 122 func (c *Bind) GetZoneRecords(domain string) (models.Records, error) { 123 foundRecords := models.Records{} 124 125 if _, err := os.Stat(c.directory); os.IsNotExist(err) { 126 fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory) 127 } 128 129 c.zonefile = filepath.Join( 130 c.directory, 131 strings.Replace(strings.ToLower(domain), "/", "_", -1)+".zone") 132 133 content, err := ioutil.ReadFile(c.zonefile) 134 if os.IsNotExist(err) { 135 // If the file doesn't exist, that's not an error. Just informational. 136 c.zoneFileFound = false 137 fmt.Fprintf(os.Stderr, "File not found: '%v'\n", c.zonefile) 138 return nil, nil 139 } 140 if err != nil { 141 return nil, fmt.Errorf("can't open %s: %w", c.zonefile, err) 142 } 143 c.zoneFileFound = true 144 145 zp := dns.NewZoneParser(strings.NewReader(string(content)), domain, c.zonefile) 146 147 for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { 148 rec := models.RRtoRC(rr, domain) 149 if rec.Type == "SOA" { 150 } 151 foundRecords = append(foundRecords, &rec) 152 } 153 154 if err := zp.Err(); err != nil { 155 return nil, fmt.Errorf("error while parsing '%v': %w", c.zonefile, err) 156 } 157 return foundRecords, nil 158 } 159 160 // GetDomainCorrections returns a list of corrections to update a domain. 161 func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 162 dc.Punycode() 163 164 comments := make([]string, 0, 5) 165 comments = append(comments, 166 fmt.Sprintf("generated with dnscontrol %s", time.Now().Format(time.RFC3339)), 167 ) 168 if dc.AutoDNSSEC { 169 comments = append(comments, "Automatic DNSSEC signing requested") 170 } 171 172 foundRecords, err := c.GetZoneRecords(dc.Name) 173 if err != nil { 174 return nil, err 175 } 176 177 // Find the SOA records; use them to make or update the desired SOA. 178 var foundSoa *models.RecordConfig 179 for _, r := range foundRecords { 180 if r.Type == "SOA" && r.Name == "@" { 181 foundSoa = r 182 break 183 } 184 } 185 var desiredSoa *models.RecordConfig 186 for _, r := range dc.Records { 187 if r.Type == "SOA" && r.Name == "@" { 188 desiredSoa = r 189 break 190 } 191 } 192 soaRec, nextSerial := makeSoa(dc.Name, &c.DefaultSoa, foundSoa, desiredSoa) 193 if desiredSoa == nil { 194 dc.Records = append(dc.Records, soaRec) 195 desiredSoa = dc.Records[len(dc.Records)-1] 196 } else { 197 *desiredSoa = *soaRec 198 } 199 200 // Normalize 201 models.PostProcessRecords(foundRecords) 202 203 differ := diff.New(dc) 204 _, create, del, mod := differ.IncrementalDiff(foundRecords) 205 206 buf := &bytes.Buffer{} 207 // Print a list of changes. Generate an actual change that is the zone 208 changes := false 209 for _, i := range create { 210 changes = true 211 if c.zoneFileFound { 212 fmt.Fprintln(buf, i) 213 } 214 } 215 for _, i := range del { 216 changes = true 217 if c.zoneFileFound { 218 fmt.Fprintln(buf, i) 219 } 220 } 221 for _, i := range mod { 222 changes = true 223 if c.zoneFileFound { 224 fmt.Fprintln(buf, i) 225 } 226 } 227 228 var msg string 229 if c.zoneFileFound { 230 msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s'. Changes:\n%s", dc.Name, buf) 231 } else { 232 msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s' (new file with %d records)\n", dc.Name, len(create)) 233 } 234 235 corrections := []*models.Correction{} 236 if changes { 237 238 // We only change the serial number if there is a change. 239 desiredSoa.SoaSerial = nextSerial 240 241 corrections = append(corrections, 242 &models.Correction{ 243 Msg: msg, 244 F: func() error { 245 fmt.Printf("WRITING ZONEFILE: %v\n", c.zonefile) 246 zf, err := os.Create(c.zonefile) 247 if err != nil { 248 return fmt.Errorf("could not create zonefile: %w", err) 249 } 250 // Beware that if there are any fake types, then they will 251 // be commented out on write, but we don't reverse that when 252 // reading, so there will be a diff on every invocation. 253 err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name, 0, comments) 254 255 if err != nil { 256 return fmt.Errorf("failed WriteZoneFile: %w", err) 257 } 258 err = zf.Close() 259 if err != nil { 260 return fmt.Errorf("closing: %w", err) 261 } 262 return nil 263 }, 264 }) 265 } 266 267 return corrections, nil 268 }