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  }