github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/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/miekg/dns/dnsutil"
    27  
    28  	"github.com/StackExchange/dnscontrol/models"
    29  	"github.com/StackExchange/dnscontrol/providers"
    30  	"github.com/StackExchange/dnscontrol/providers/diff"
    31  )
    32  
    33  var docNotes = providers.DocumentationNotes{
    34  	providers.DocDualHost:            providers.Can(),
    35  	providers.DocCreateDomains:       providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."),
    36  	providers.DocOfficiallySupported: providers.Can(),
    37  }
    38  
    39  func initBind(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
    40  	// config -- the key/values from creds.json
    41  	// meta -- the json blob from NewReq('name', 'TYPE', meta)
    42  	api := &Bind{
    43  		directory: config["directory"],
    44  	}
    45  	if api.directory == "" {
    46  		api.directory = "zones"
    47  	}
    48  	if len(providermeta) != 0 {
    49  		err := json.Unmarshal(providermeta, api)
    50  		if err != nil {
    51  			return nil, err
    52  		}
    53  	}
    54  	api.nameservers = models.StringsToNameservers(api.DefaultNS)
    55  	return api, nil
    56  }
    57  
    58  func init() {
    59  	providers.RegisterDomainServiceProviderType("BIND", initBind, providers.CanUsePTR,
    60  		providers.CanUseSRV, providers.CanUseCAA, providers.CanUseTLSA, providers.CantUseNOPURGE, docNotes)
    61  }
    62  
    63  type SoaInfo struct {
    64  	Ns      string `json:"master"`
    65  	Mbox    string `json:"mbox"`
    66  	Serial  uint32 `json:"serial"`
    67  	Refresh uint32 `json:"refresh"`
    68  	Retry   uint32 `json:"retry"`
    69  	Expire  uint32 `json:"expire"`
    70  	Minttl  uint32 `json:"minttl"`
    71  }
    72  
    73  func (s SoaInfo) String() string {
    74  	return fmt.Sprintf("%s %s %d %d %d %d %d", s.Ns, s.Mbox, s.Serial, s.Refresh, s.Retry, s.Expire, s.Minttl)
    75  }
    76  
    77  type Bind struct {
    78  	DefaultNS   []string `json:"default_ns"`
    79  	DefaultSoa  SoaInfo  `json:"default_soa"`
    80  	nameservers []*models.Nameserver
    81  	directory   string
    82  }
    83  
    84  //var bindSkeletin = flag.String("bind_skeletin", "skeletin/master/var/named/chroot/var/named/master", "")
    85  
    86  func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordConfig, uint32) {
    87  	// Convert's dns.RR into our native data type (models.RecordConfig).
    88  	// Records are translated directly with no changes.
    89  	// If it is an SOA for the apex domain and
    90  	// replaceSerial != 0, change the serial to replaceSerial.
    91  	// WARNING(tlim): This assumes SOAs do not have serial=0.
    92  	// If one is found, we replace it with serial=1.
    93  	var old_serial, new_serial uint32
    94  	header := rr.Header()
    95  	rc := models.RecordConfig{}
    96  	rc.Type = dns.TypeToString[header.Rrtype]
    97  	rc.NameFQDN = strings.ToLower(strings.TrimSuffix(header.Name, "."))
    98  	rc.Name = strings.ToLower(dnsutil.TrimDomainName(header.Name, origin))
    99  	rc.TTL = header.Ttl
   100  	switch v := rr.(type) { // #rtype_variations
   101  	case *dns.A:
   102  		rc.Target = v.A.String()
   103  	case *dns.AAAA:
   104  		rc.Target = v.AAAA.String()
   105  	case *dns.CAA:
   106  		rc.CaaTag = v.Tag
   107  		rc.CaaFlag = v.Flag
   108  		rc.Target = v.Value
   109  	case *dns.CNAME:
   110  		rc.Target = v.Target
   111  	case *dns.MX:
   112  		rc.Target = v.Mx
   113  		rc.MxPreference = v.Preference
   114  	case *dns.NS:
   115  		rc.Target = v.Ns
   116  	case *dns.PTR:
   117  		rc.Target = v.Ptr
   118  	case *dns.SOA:
   119  		old_serial = v.Serial
   120  		if old_serial == 0 {
   121  			// For SOA records, we never return a 0 serial number.
   122  			old_serial = 1
   123  		}
   124  		new_serial = v.Serial
   125  		if rc.Name == "@" && replaceSerial != 0 {
   126  			new_serial = replaceSerial
   127  		}
   128  		rc.Target = fmt.Sprintf("%v %v %v %v %v %v %v",
   129  			v.Ns, v.Mbox, new_serial, v.Refresh, v.Retry, v.Expire, v.Minttl)
   130  	case *dns.SRV:
   131  		rc.Target = v.Target
   132  		rc.SrvPort = v.Port
   133  		rc.SrvWeight = v.Weight
   134  		rc.SrvPriority = v.Priority
   135  	case *dns.TLSA:
   136  		rc.TlsaUsage = v.Usage
   137  		rc.TlsaSelector = v.Selector
   138  		rc.TlsaMatchingType = v.MatchingType
   139  		rc.Target = v.Certificate
   140  	case *dns.TXT:
   141  		rc.Target = strings.Join(v.Txt, " ")
   142  	default:
   143  		log.Fatalf("rrToRecord: Unimplemented zone record type=%s (%v)\n", rc.Type, rr)
   144  	}
   145  	return rc, old_serial
   146  }
   147  
   148  func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
   149  	// Make a default SOA record in case one isn't found:
   150  	soaRec := models.RecordConfig{
   151  		Type: "SOA",
   152  		Name: "@",
   153  	}
   154  	soaRec.NameFQDN = dnsutil.AddOrigin(soaRec.Name, origin)
   155  	if len(info.Ns) == 0 {
   156  		info.Ns = "DEFAULT_NOT_SET."
   157  	}
   158  	if len(info.Mbox) == 0 {
   159  		info.Mbox = "DEFAULT_NOT_SET."
   160  	}
   161  	if info.Serial == 0 {
   162  		info.Serial = 1
   163  	}
   164  	if info.Refresh == 0 {
   165  		info.Refresh = 3600
   166  	}
   167  	if info.Retry == 0 {
   168  		info.Retry = 600
   169  	}
   170  	if info.Expire == 0 {
   171  		info.Expire = 604800
   172  	}
   173  	if info.Minttl == 0 {
   174  		info.Minttl = 1440
   175  	}
   176  	soaRec.Target = info.String()
   177  
   178  	return &soaRec
   179  }
   180  
   181  func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
   182  	return c.nameservers, nil
   183  }
   184  
   185  func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   186  	dc.Punycode()
   187  	// Phase 1: Copy everything to []*models.RecordConfig:
   188  	//    expectedRecords < dc.Records[i]
   189  	//    foundRecords < zonefile
   190  	//
   191  	// Phase 2: Do any manipulations:
   192  	// add NS
   193  	// manipulate SOA
   194  	//
   195  	// Phase 3: Convert to []diff.Records and compare:
   196  	// expectedDiffRecords < expectedRecords
   197  	// foundDiffRecords < foundRecords
   198  	// diff.Inc...(foundDiffRecords, expectedDiffRecords )
   199  
   200  	// Default SOA record.  If we see one in the zone, this will be replaced.
   201  	soaRec := makeDefaultSOA(c.DefaultSoa, dc.Name)
   202  
   203  	// Read foundRecords:
   204  	foundRecords := make([]*models.RecordConfig, 0)
   205  	var oldSerial, newSerial uint32
   206  	zonefile := filepath.Join(c.directory, strings.Replace(strings.ToLower(dc.Name), "/", "_", -1)+".zone")
   207  	foundFH, err := os.Open(zonefile)
   208  	zoneFileFound := err == nil
   209  	if err != nil && !os.IsNotExist(os.ErrNotExist) {
   210  		// Don't whine if the file doesn't exist. However all other
   211  		// errors will be reported.
   212  		fmt.Printf("Could not read zonefile: %v\n", err)
   213  	} else {
   214  		for x := range dns.ParseZone(foundFH, dc.Name, zonefile) {
   215  			if x.Error != nil {
   216  				log.Println("Error in zonefile:", x.Error)
   217  			} else {
   218  				rec, serial := rrToRecord(x.RR, dc.Name, oldSerial)
   219  				if serial != 0 && oldSerial != 0 {
   220  					log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile)
   221  				}
   222  				if serial != 0 {
   223  					// This was an SOA record. Update the serial.
   224  					oldSerial = serial
   225  					newSerial = generate_serial(oldSerial)
   226  					// Regenerate with new serial:
   227  					*soaRec, _ = rrToRecord(x.RR, dc.Name, newSerial)
   228  					rec = *soaRec
   229  				}
   230  				foundRecords = append(foundRecords, &rec)
   231  			}
   232  		}
   233  	}
   234  
   235  	// Add SOA record to expected set:
   236  	if !dc.HasRecordTypeName("SOA", "@") {
   237  		dc.Records = append(dc.Records, soaRec)
   238  	}
   239  
   240  	differ := diff.New(dc)
   241  	_, create, del, mod := differ.IncrementalDiff(foundRecords)
   242  
   243  	buf := &bytes.Buffer{}
   244  	// Print a list of changes. Generate an actual change that is the zone
   245  	changes := false
   246  	for _, i := range create {
   247  		changes = true
   248  		if zoneFileFound {
   249  			fmt.Fprintln(buf, i)
   250  		}
   251  	}
   252  	for _, i := range del {
   253  		changes = true
   254  		if zoneFileFound {
   255  			fmt.Fprintln(buf, i)
   256  		}
   257  	}
   258  	for _, i := range mod {
   259  		changes = true
   260  		if zoneFileFound {
   261  			fmt.Fprintln(buf, i)
   262  		}
   263  	}
   264  	msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name)
   265  	if !zoneFileFound {
   266  		msg = msg + fmt.Sprintf(" (%d records)\n", len(create))
   267  	}
   268  	msg += buf.String()
   269  	corrections := []*models.Correction{}
   270  	if changes {
   271  		corrections = append(corrections,
   272  			&models.Correction{
   273  				Msg: msg,
   274  				F: func() error {
   275  					fmt.Printf("CREATING ZONEFILE: %v\n", zonefile)
   276  					zf, err := os.Create(zonefile)
   277  					if err != nil {
   278  						log.Fatalf("Could not create zonefile: %v", err)
   279  					}
   280  					zonefilerecords := make([]dns.RR, 0, len(dc.Records))
   281  					for _, r := range dc.Records {
   282  						zonefilerecords = append(zonefilerecords, r.ToRR())
   283  					}
   284  					err = WriteZoneFile(zf, zonefilerecords, dc.Name)
   285  
   286  					if err != nil {
   287  						log.Fatalf("WriteZoneFile error: %v\n", err)
   288  					}
   289  					err = zf.Close()
   290  					if err != nil {
   291  						log.Fatalf("Closing: %v", err)
   292  					}
   293  					return nil
   294  				},
   295  			})
   296  	}
   297  
   298  	return corrections, nil
   299  }