mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
BIND: Improve SOA serial number handling (#651)
* github.com/miekg/dns * Greatly simplify the logic for handling serial numbers. Related code was all over the place. Now it is abstracted into one testable method makeSoa. This simplifies code in many other places. * Update docs/_providers/bind.md: Edit old text. Add SOA description. * SOA records are now treated like any other record internally. You still can't specify them in dnsconfig.js, but that's by design. * The URL for issue 491 was wrong in many places * BIND: Clarify GENERATE_ZONEFILE message
This commit is contained in:
@ -17,7 +17,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -70,7 +70,7 @@ func init() {
|
||||
providers.RegisterDomainServiceProviderType("BIND", initBind, features)
|
||||
}
|
||||
|
||||
// SoaInfo contains the parts of a SOA rtype.
|
||||
// SoaInfo contains the parts of the default SOA settings.
|
||||
type SoaInfo struct {
|
||||
Ns string `json:"master"`
|
||||
Mbox string `json:"mbox"`
|
||||
@ -95,38 +95,6 @@ type Bind struct {
|
||||
zoneFileFound bool // Did the zonefile exist?
|
||||
}
|
||||
|
||||
func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
|
||||
// Make a default SOA record in case one isn't found:
|
||||
soaRec := models.RecordConfig{
|
||||
Type: "SOA",
|
||||
}
|
||||
soaRec.SetLabel("@", origin)
|
||||
if len(info.Ns) == 0 {
|
||||
info.Ns = "DEFAULT_NOT_SET."
|
||||
}
|
||||
if len(info.Mbox) == 0 {
|
||||
info.Mbox = "DEFAULT_NOT_SET."
|
||||
}
|
||||
if info.Serial == 0 {
|
||||
info.Serial = 1
|
||||
}
|
||||
if info.Refresh == 0 {
|
||||
info.Refresh = 3600
|
||||
}
|
||||
if info.Retry == 0 {
|
||||
info.Retry = 600
|
||||
}
|
||||
if info.Expire == 0 {
|
||||
info.Expire = 604800
|
||||
}
|
||||
if info.Minttl == 0 {
|
||||
info.Minttl = 1440
|
||||
}
|
||||
soaRec.SetTarget(info.String())
|
||||
|
||||
return &soaRec
|
||||
}
|
||||
|
||||
// GetNameservers returns the nameservers for a domain.
|
||||
func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
|
||||
return c.nameservers, nil
|
||||
@ -135,7 +103,7 @@ func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
|
||||
// ListZones returns all the zones in an account
|
||||
func (c *Bind) ListZones() ([]string, error) {
|
||||
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("BIND directory %q does not exist!\n", c.directory)
|
||||
return nil, fmt.Errorf("directory %q does not exist", c.directory)
|
||||
}
|
||||
|
||||
filenames, err := filepath.Glob(filepath.Join(c.directory, "*.zone"))
|
||||
@ -152,82 +120,81 @@ func (c *Bind) ListZones() ([]string, error) {
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
func (c *Bind) GetZoneRecords(domain string) (models.Records, error) {
|
||||
|
||||
// Default SOA record. If we see one in the zone, this will be replaced.
|
||||
soaRec := makeDefaultSOA(c.DefaultSoa, domain)
|
||||
foundRecords := models.Records{}
|
||||
var oldSerial, newSerial uint32
|
||||
|
||||
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
||||
fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
|
||||
}
|
||||
|
||||
zonefile := filepath.Join(c.directory, strings.Replace(strings.ToLower(domain), "/", "_", -1)+".zone")
|
||||
c.zonefile = zonefile
|
||||
foundFH, err := os.Open(zonefile)
|
||||
c.zoneFileFound = err == nil
|
||||
if err != nil && !os.IsNotExist(os.ErrNotExist) {
|
||||
// Don't whine if the file doesn't exist. However all other
|
||||
// errors will be reported.
|
||||
fmt.Printf("Could not read zonefile: %v\n", err)
|
||||
} else {
|
||||
for x := range dns.ParseZone(foundFH, domain, zonefile) {
|
||||
if x.Error != nil {
|
||||
log.Println("Error in zonefile:", x.Error)
|
||||
} else {
|
||||
rec, serial := models.RRtoRC(x.RR, domain, oldSerial)
|
||||
if serial != 0 && oldSerial != 0 {
|
||||
log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile)
|
||||
}
|
||||
if serial != 0 {
|
||||
// This was an SOA record. Update the serial.
|
||||
oldSerial = serial
|
||||
newSerial = generateSerial(oldSerial)
|
||||
// Regenerate with new serial:
|
||||
*soaRec, _ = models.RRtoRC(x.RR, domain, newSerial)
|
||||
rec = *soaRec
|
||||
}
|
||||
foundRecords = append(foundRecords, &rec)
|
||||
}
|
||||
c.zonefile = filepath.Join(
|
||||
c.directory,
|
||||
strings.Replace(strings.ToLower(domain), "/", "_", -1)+".zone")
|
||||
|
||||
content, err := ioutil.ReadFile(c.zonefile)
|
||||
if os.IsNotExist(err) {
|
||||
// If the file doesn't exist, that's not an error. Just informational.
|
||||
c.zoneFileFound = false
|
||||
fmt.Fprintf(os.Stderr, "File not found: '%v'\n", c.zonefile)
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't open %s: %w", c.zonefile, err)
|
||||
}
|
||||
c.zoneFileFound = true
|
||||
|
||||
zp := dns.NewZoneParser(strings.NewReader(string(content)), domain, c.zonefile)
|
||||
|
||||
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
||||
rec := models.RRtoRC(rr, domain)
|
||||
if rec.Type == "SOA" {
|
||||
}
|
||||
foundRecords = append(foundRecords, &rec)
|
||||
}
|
||||
|
||||
// Add SOA record to expected set:
|
||||
if !foundRecords.HasRecordTypeName("SOA", "@") {
|
||||
//foundRecords = append(foundRecords, soaRec)
|
||||
if err := zp.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error while parsing '%v': %w", c.zonefile, err)
|
||||
}
|
||||
|
||||
return foundRecords, nil
|
||||
}
|
||||
|
||||
// GetDomainCorrections returns a list of corrections to update a domain.
|
||||
func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
// Phase 1: Copy everything to []*models.RecordConfig:
|
||||
// expectedRecords < dc.Records[i]
|
||||
// foundRecords < zonefile
|
||||
//
|
||||
// Phase 2: Do any manipulations:
|
||||
// add NS
|
||||
// manipulate SOA
|
||||
//
|
||||
// Phase 3: Convert to []diff.Records and compare:
|
||||
// expectedDiffRecords < expectedRecords
|
||||
// foundDiffRecords < foundRecords
|
||||
// diff.Inc...(foundDiffRecords, expectedDiffRecords )
|
||||
|
||||
comments := make([]string, 0, 5)
|
||||
|
||||
comments = append(comments,
|
||||
fmt.Sprintf("generated with dnscontrol %s", time.Now().Format(time.RFC3339)),
|
||||
)
|
||||
if dc.AutoDNSSEC {
|
||||
comments = append(comments, "Automatic DNSSEC signing requested")
|
||||
}
|
||||
|
||||
foundRecords, err := c.GetZoneRecords(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dc.AutoDNSSEC {
|
||||
comments = append(comments, "Automatic DNSSEC signing requested")
|
||||
|
||||
// Find the SOA records; use them to make or update the desired SOA.
|
||||
var foundSoa *models.RecordConfig
|
||||
for _, r := range foundRecords {
|
||||
if r.Type == "SOA" && r.Name == "@" {
|
||||
foundSoa = r
|
||||
break
|
||||
}
|
||||
}
|
||||
var desiredSoa *models.RecordConfig
|
||||
for _, r := range dc.Records {
|
||||
if r.Type == "SOA" && r.Name == "@" {
|
||||
desiredSoa = r
|
||||
break
|
||||
}
|
||||
}
|
||||
soaRec, nextSerial := makeSoa(dc.Name, &c.DefaultSoa, foundSoa, desiredSoa)
|
||||
if desiredSoa == nil {
|
||||
dc.Records = append(dc.Records, soaRec)
|
||||
desiredSoa = dc.Records[len(dc.Records)-1]
|
||||
} else {
|
||||
*desiredSoa = *soaRec
|
||||
}
|
||||
|
||||
// Normalize
|
||||
@ -257,21 +224,28 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
|
||||
fmt.Fprintln(buf, i)
|
||||
}
|
||||
}
|
||||
msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name)
|
||||
if !c.zoneFileFound {
|
||||
msg = msg + fmt.Sprintf(" (%d records)\n", len(create))
|
||||
|
||||
var msg string
|
||||
if c.zoneFileFound {
|
||||
msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s'. Changes:\n%s", dc.Name, buf)
|
||||
} else {
|
||||
msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s' (new file with %d records)\n", dc.Name, len(create))
|
||||
}
|
||||
msg += buf.String()
|
||||
|
||||
corrections := []*models.Correction{}
|
||||
if changes {
|
||||
|
||||
// We only change the serial number if there is a change.
|
||||
desiredSoa.SoaSerial = nextSerial
|
||||
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
fmt.Printf("CREATING ZONEFILE: %v\n", c.zonefile)
|
||||
fmt.Printf("WRITING ZONEFILE: %v\n", c.zonefile)
|
||||
zf, err := os.Create(c.zonefile)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create zonefile: %v", err)
|
||||
return fmt.Errorf("could not create zonefile: %w", err)
|
||||
}
|
||||
// Beware that if there are any fake types, then they will
|
||||
// be commented out on write, but we don't reverse that when
|
||||
@ -279,11 +253,11 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
|
||||
err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name, comments)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("WriteZoneFile error: %v\n", err)
|
||||
return fmt.Errorf("failed WriteZoneFile: %w", err)
|
||||
}
|
||||
err = zf.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("Closing: %v", err)
|
||||
return fmt.Errorf("closing: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
Reference in New Issue
Block a user