github.com/teknogeek/dnscontrol@v0.2.8/providers/bind/bindProvider.go (about) 1 package bind 2 3 /* 4 5 bind - 6 Generate zonefiles suitiable 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 "log" 21 "os" 22 "path/filepath" 23 "strings" 24 25 "github.com/miekg/dns" 26 "github.com/pkg/errors" 27 28 "github.com/StackExchange/dnscontrol/models" 29 "github.com/StackExchange/dnscontrol/providers" 30 "github.com/StackExchange/dnscontrol/providers/diff" 31 ) 32 33 var features = providers.DocumentationNotes{ 34 providers.CanUseCAA: providers.Can(), 35 providers.CanUsePTR: providers.Can(), 36 providers.CanUseSRV: providers.Can(), 37 providers.CanUseTLSA: providers.Can(), 38 providers.CanUseTXTMulti: providers.Can(), 39 providers.CantUseNOPURGE: providers.Cannot(), 40 providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."), 41 providers.DocDualHost: providers.Can(), 42 providers.DocOfficiallySupported: providers.Can(), 43 } 44 45 func initBind(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) { 46 // config -- the key/values from creds.json 47 // meta -- the json blob from NewReq('name', 'TYPE', meta) 48 api := &Bind{ 49 directory: config["directory"], 50 } 51 if api.directory == "" { 52 api.directory = "zones" 53 } 54 if len(providermeta) != 0 { 55 err := json.Unmarshal(providermeta, api) 56 if err != nil { 57 return nil, err 58 } 59 } 60 api.nameservers = models.StringsToNameservers(api.DefaultNS) 61 return api, nil 62 } 63 64 func init() { 65 providers.RegisterDomainServiceProviderType("BIND", initBind, features) 66 } 67 68 // SoaInfo contains the parts of a SOA rtype. 69 type SoaInfo struct { 70 Ns string `json:"master"` 71 Mbox string `json:"mbox"` 72 Serial uint32 `json:"serial"` 73 Refresh uint32 `json:"refresh"` 74 Retry uint32 `json:"retry"` 75 Expire uint32 `json:"expire"` 76 Minttl uint32 `json:"minttl"` 77 } 78 79 func (s SoaInfo) String() string { 80 return fmt.Sprintf("%s %s %d %d %d %d %d", s.Ns, s.Mbox, s.Serial, s.Refresh, s.Retry, s.Expire, s.Minttl) 81 } 82 83 // Bind is the provider handle for the Bind driver. 84 type Bind struct { 85 DefaultNS []string `json:"default_ns"` 86 DefaultSoa SoaInfo `json:"default_soa"` 87 nameservers []*models.Nameserver 88 directory string 89 } 90 91 // var bindSkeletin = flag.String("bind_skeletin", "skeletin/master/var/named/chroot/var/named/master", "") 92 93 func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordConfig, uint32) { 94 // Convert's dns.RR into our native data type (models.RecordConfig). 95 // Records are translated directly with no changes. 96 // If it is an SOA for the apex domain and 97 // replaceSerial != 0, change the serial to replaceSerial. 98 // WARNING(tlim): This assumes SOAs do not have serial=0. 99 // If one is found, we replace it with serial=1. 100 var oldSerial, newSerial uint32 101 header := rr.Header() 102 rc := models.RecordConfig{ 103 Type: dns.TypeToString[header.Rrtype], 104 TTL: header.Ttl, 105 } 106 rc.SetLabelFromFQDN(strings.TrimSuffix(header.Name, "."), origin) 107 switch v := rr.(type) { // #rtype_variations 108 case *dns.A: 109 panicInvalid(rc.SetTarget(v.A.String())) 110 case *dns.AAAA: 111 panicInvalid(rc.SetTarget(v.AAAA.String())) 112 case *dns.CAA: 113 panicInvalid(rc.SetTargetCAA(v.Flag, v.Tag, v.Value)) 114 case *dns.CNAME: 115 panicInvalid(rc.SetTarget(v.Target)) 116 case *dns.MX: 117 panicInvalid(rc.SetTargetMX(v.Preference, v.Mx)) 118 case *dns.NS: 119 panicInvalid(rc.SetTarget(v.Ns)) 120 case *dns.PTR: 121 panicInvalid(rc.SetTarget(v.Ptr)) 122 case *dns.SOA: 123 oldSerial = v.Serial 124 if oldSerial == 0 { 125 // For SOA records, we never return a 0 serial number. 126 oldSerial = 1 127 } 128 newSerial = v.Serial 129 //if (dnsutil.TrimDomainName(rc.Name, origin+".") == "@") && replaceSerial != 0 { 130 if rc.GetLabel() == "@" && replaceSerial != 0 { 131 newSerial = replaceSerial 132 } 133 panicInvalid(rc.SetTarget( 134 fmt.Sprintf("%v %v %v %v %v %v %v", 135 v.Ns, v.Mbox, newSerial, v.Refresh, v.Retry, v.Expire, v.Minttl), 136 )) 137 // FIXME(tlim): SOA should be handled by splitting out the fields. 138 case *dns.SRV: 139 panicInvalid(rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target)) 140 case *dns.TLSA: 141 panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate)) 142 case *dns.TXT: 143 panicInvalid(rc.SetTargetTXTs(v.Txt)) 144 default: 145 log.Fatalf("rrToRecord: Unimplemented zone record type=%s (%v)\n", rc.Type, rr) 146 } 147 return rc, oldSerial 148 } 149 150 func panicInvalid(err error) { 151 if err != nil { 152 panic(errors.Wrap(err, "unparsable record received from BIND")) 153 } 154 } 155 156 func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig { 157 // Make a default SOA record in case one isn't found: 158 soaRec := models.RecordConfig{ 159 Type: "SOA", 160 } 161 soaRec.SetLabel("@", origin) 162 if len(info.Ns) == 0 { 163 info.Ns = "DEFAULT_NOT_SET." 164 } 165 if len(info.Mbox) == 0 { 166 info.Mbox = "DEFAULT_NOT_SET." 167 } 168 if info.Serial == 0 { 169 info.Serial = 1 170 } 171 if info.Refresh == 0 { 172 info.Refresh = 3600 173 } 174 if info.Retry == 0 { 175 info.Retry = 600 176 } 177 if info.Expire == 0 { 178 info.Expire = 604800 179 } 180 if info.Minttl == 0 { 181 info.Minttl = 1440 182 } 183 soaRec.SetTarget(info.String()) 184 185 return &soaRec 186 } 187 188 // GetNameservers returns the nameservers for a domain. 189 func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) { 190 return c.nameservers, nil 191 } 192 193 // GetDomainCorrections returns a list of corrections to update a domain. 194 func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 195 dc.Punycode() 196 // Phase 1: Copy everything to []*models.RecordConfig: 197 // expectedRecords < dc.Records[i] 198 // foundRecords < zonefile 199 // 200 // Phase 2: Do any manipulations: 201 // add NS 202 // manipulate SOA 203 // 204 // Phase 3: Convert to []diff.Records and compare: 205 // expectedDiffRecords < expectedRecords 206 // foundDiffRecords < foundRecords 207 // diff.Inc...(foundDiffRecords, expectedDiffRecords ) 208 209 // Default SOA record. If we see one in the zone, this will be replaced. 210 soaRec := makeDefaultSOA(c.DefaultSoa, dc.Name) 211 212 // Read foundRecords: 213 foundRecords := make([]*models.RecordConfig, 0) 214 var oldSerial, newSerial uint32 215 216 if _, err := os.Stat(c.directory); os.IsNotExist(err) { 217 fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory) 218 } 219 220 zonefile := filepath.Join(c.directory, strings.Replace(strings.ToLower(dc.Name), "/", "_", -1)+".zone") 221 foundFH, err := os.Open(zonefile) 222 zoneFileFound := err == nil 223 if err != nil && !os.IsNotExist(os.ErrNotExist) { 224 // Don't whine if the file doesn't exist. However all other 225 // errors will be reported. 226 fmt.Printf("Could not read zonefile: %v\n", err) 227 } else { 228 for x := range dns.ParseZone(foundFH, dc.Name, zonefile) { 229 if x.Error != nil { 230 log.Println("Error in zonefile:", x.Error) 231 } else { 232 rec, serial := rrToRecord(x.RR, dc.Name, oldSerial) 233 if serial != 0 && oldSerial != 0 { 234 log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile) 235 } 236 if serial != 0 { 237 // This was an SOA record. Update the serial. 238 oldSerial = serial 239 newSerial = generateSerial(oldSerial) 240 // Regenerate with new serial: 241 *soaRec, _ = rrToRecord(x.RR, dc.Name, newSerial) 242 rec = *soaRec 243 } 244 foundRecords = append(foundRecords, &rec) 245 } 246 } 247 } 248 249 // Add SOA record to expected set: 250 if !dc.HasRecordTypeName("SOA", "@") { 251 dc.Records = append(dc.Records, soaRec) 252 } 253 254 // Normalize 255 models.PostProcessRecords(foundRecords) 256 257 differ := diff.New(dc) 258 _, create, del, mod := differ.IncrementalDiff(foundRecords) 259 260 buf := &bytes.Buffer{} 261 // Print a list of changes. Generate an actual change that is the zone 262 changes := false 263 for _, i := range create { 264 changes = true 265 if zoneFileFound { 266 fmt.Fprintln(buf, i) 267 } 268 } 269 for _, i := range del { 270 changes = true 271 if zoneFileFound { 272 fmt.Fprintln(buf, i) 273 } 274 } 275 for _, i := range mod { 276 changes = true 277 if zoneFileFound { 278 fmt.Fprintln(buf, i) 279 } 280 } 281 msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name) 282 if !zoneFileFound { 283 msg = msg + fmt.Sprintf(" (%d records)\n", len(create)) 284 } 285 msg += buf.String() 286 corrections := []*models.Correction{} 287 if changes { 288 corrections = append(corrections, 289 &models.Correction{ 290 Msg: msg, 291 F: func() error { 292 fmt.Printf("CREATING ZONEFILE: %v\n", zonefile) 293 zf, err := os.Create(zonefile) 294 if err != nil { 295 log.Fatalf("Could not create zonefile: %v", err) 296 } 297 zonefilerecords := make([]dns.RR, 0, len(dc.Records)) 298 for _, r := range dc.Records { 299 zonefilerecords = append(zonefilerecords, r.ToRR()) 300 } 301 err = WriteZoneFile(zf, zonefilerecords, dc.Name) 302 303 if err != nil { 304 log.Fatalf("WriteZoneFile error: %v\n", err) 305 } 306 err = zf.Close() 307 if err != nil { 308 log.Fatalf("Closing: %v", err) 309 } 310 return nil 311 }, 312 }) 313 } 314 315 return corrections, nil 316 }