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  }