github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/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 zonefile := filepath.Join(c.directory, strings.Replace(strings.ToLower(dc.Name), "/", "_", -1)+".zone") 216 foundFH, err := os.Open(zonefile) 217 zoneFileFound := err == nil 218 if err != nil && !os.IsNotExist(os.ErrNotExist) { 219 // Don't whine if the file doesn't exist. However all other 220 // errors will be reported. 221 fmt.Printf("Could not read zonefile: %v\n", err) 222 } else { 223 for x := range dns.ParseZone(foundFH, dc.Name, zonefile) { 224 if x.Error != nil { 225 log.Println("Error in zonefile:", x.Error) 226 } else { 227 rec, serial := rrToRecord(x.RR, dc.Name, oldSerial) 228 if serial != 0 && oldSerial != 0 { 229 log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile) 230 } 231 if serial != 0 { 232 // This was an SOA record. Update the serial. 233 oldSerial = serial 234 newSerial = generateSerial(oldSerial) 235 // Regenerate with new serial: 236 *soaRec, _ = rrToRecord(x.RR, dc.Name, newSerial) 237 rec = *soaRec 238 } 239 foundRecords = append(foundRecords, &rec) 240 } 241 } 242 } 243 244 // Add SOA record to expected set: 245 if !dc.HasRecordTypeName("SOA", "@") { 246 dc.Records = append(dc.Records, soaRec) 247 } 248 249 // Normalize 250 models.PostProcessRecords(foundRecords) 251 252 differ := diff.New(dc) 253 _, create, del, mod := differ.IncrementalDiff(foundRecords) 254 255 buf := &bytes.Buffer{} 256 // Print a list of changes. Generate an actual change that is the zone 257 changes := false 258 for _, i := range create { 259 changes = true 260 if zoneFileFound { 261 fmt.Fprintln(buf, i) 262 } 263 } 264 for _, i := range del { 265 changes = true 266 if zoneFileFound { 267 fmt.Fprintln(buf, i) 268 } 269 } 270 for _, i := range mod { 271 changes = true 272 if zoneFileFound { 273 fmt.Fprintln(buf, i) 274 } 275 } 276 msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name) 277 if !zoneFileFound { 278 msg = msg + fmt.Sprintf(" (%d records)\n", len(create)) 279 } 280 msg += buf.String() 281 corrections := []*models.Correction{} 282 if changes { 283 corrections = append(corrections, 284 &models.Correction{ 285 Msg: msg, 286 F: func() error { 287 fmt.Printf("CREATING ZONEFILE: %v\n", zonefile) 288 zf, err := os.Create(zonefile) 289 if err != nil { 290 log.Fatalf("Could not create zonefile: %v", err) 291 } 292 zonefilerecords := make([]dns.RR, 0, len(dc.Records)) 293 for _, r := range dc.Records { 294 zonefilerecords = append(zonefilerecords, r.ToRR()) 295 } 296 err = WriteZoneFile(zf, zonefilerecords, dc.Name) 297 298 if err != nil { 299 log.Fatalf("WriteZoneFile error: %v\n", err) 300 } 301 err = zf.Close() 302 if err != nil { 303 log.Fatalf("Closing: %v", err) 304 } 305 return nil 306 }, 307 }) 308 } 309 310 return corrections, nil 311 }