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  }