2016-08-23 08:31:50 +08:00
|
|
|
package bind
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
bind -
|
2020-02-23 02:27:24 +08:00
|
|
|
Generate zonefiles suitable for BIND.
|
2016-08-23 08:31:50 +08:00
|
|
|
|
|
|
|
The zonefiles are read and written to the directory -bind_dir
|
|
|
|
|
|
|
|
If the old zonefiles are readable, we read them to determine
|
|
|
|
if an update is actually needed. The old zonefile is also used
|
|
|
|
as the basis for generating the new SOA serial number.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
import (
|
2017-03-15 05:47:40 +08:00
|
|
|
"bytes"
|
2016-08-23 08:31:50 +08:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2020-02-23 02:27:24 +08:00
|
|
|
"time"
|
2016-08-23 08:31:50 +08:00
|
|
|
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
|
2020-01-28 23:42:31 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v2/models"
|
2020-02-18 21:59:18 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v2/pkg/prettyzone"
|
2020-01-28 23:42:31 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v2/providers"
|
|
|
|
"github.com/StackExchange/dnscontrol/v2/providers/diff"
|
2016-08-23 08:31:50 +08:00
|
|
|
)
|
|
|
|
|
2018-01-05 08:19:35 +08:00
|
|
|
var features = providers.DocumentationNotes{
|
|
|
|
providers.CanUseCAA: providers.Can(),
|
|
|
|
providers.CanUsePTR: providers.Can(),
|
2019-03-29 19:01:52 +08:00
|
|
|
providers.CanUseNAPTR: providers.Can(),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.CanUseSRV: providers.Can(),
|
2019-01-29 06:26:20 +08:00
|
|
|
providers.CanUseSSHFP: providers.Can(),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.CanUseTLSA: providers.Can(),
|
|
|
|
providers.CanUseTXTMulti: providers.Can(),
|
2020-02-23 02:27:24 +08:00
|
|
|
providers.CanAutoDNSSEC: providers.Can("Just writes out a comment indicating DNSSEC was requested"),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.CantUseNOPURGE: providers.Cannot(),
|
2017-09-15 04:13:17 +08:00
|
|
|
providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.DocDualHost: providers.Can(),
|
2017-09-15 04:13:17 +08:00
|
|
|
providers.DocOfficiallySupported: providers.Can(),
|
2020-02-18 21:59:18 +08:00
|
|
|
providers.CanGetZones: providers.Can(),
|
2017-09-15 04:13:17 +08:00
|
|
|
}
|
|
|
|
|
2017-07-06 22:24:21 +08:00
|
|
|
func initBind(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
|
|
// config -- the key/values from creds.json
|
|
|
|
// meta -- the json blob from NewReq('name', 'TYPE', meta)
|
2017-09-13 22:00:41 +08:00
|
|
|
api := &Bind{
|
|
|
|
directory: config["directory"],
|
|
|
|
}
|
|
|
|
if api.directory == "" {
|
|
|
|
api.directory = "zones"
|
|
|
|
}
|
2017-07-06 22:24:21 +08:00
|
|
|
if len(providermeta) != 0 {
|
|
|
|
err := json.Unmarshal(providermeta, api)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
api.nameservers = models.StringsToNameservers(api.DefaultNS)
|
|
|
|
return api, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.RegisterDomainServiceProviderType("BIND", initBind, features)
|
2017-07-06 22:24:21 +08:00
|
|
|
}
|
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// SoaInfo contains the parts of a SOA rtype.
|
2016-08-23 08:31:50 +08:00
|
|
|
type SoaInfo struct {
|
|
|
|
Ns string `json:"master"`
|
|
|
|
Mbox string `json:"mbox"`
|
|
|
|
Serial uint32 `json:"serial"`
|
|
|
|
Refresh uint32 `json:"refresh"`
|
|
|
|
Retry uint32 `json:"retry"`
|
|
|
|
Expire uint32 `json:"expire"`
|
|
|
|
Minttl uint32 `json:"minttl"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s SoaInfo) String() string {
|
|
|
|
return fmt.Sprintf("%s %s %d %d %d %d %d", s.Ns, s.Mbox, s.Serial, s.Refresh, s.Retry, s.Expire, s.Minttl)
|
|
|
|
}
|
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// Bind is the provider handle for the Bind driver.
|
2016-08-23 08:31:50 +08:00
|
|
|
type Bind struct {
|
2020-02-18 21:59:18 +08:00
|
|
|
DefaultNS []string `json:"default_ns"`
|
|
|
|
DefaultSoa SoaInfo `json:"default_soa"`
|
|
|
|
nameservers []*models.Nameserver
|
|
|
|
directory string
|
|
|
|
zonefile string // Where the zone data is expected
|
|
|
|
zoneFileFound bool // Did the zonefile exist?
|
2018-02-16 01:02:50 +08:00
|
|
|
}
|
|
|
|
|
2016-08-23 08:31:50 +08:00
|
|
|
func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
|
|
|
|
// Make a default SOA record in case one isn't found:
|
2017-03-23 00:38:08 +08:00
|
|
|
soaRec := models.RecordConfig{
|
2016-08-23 08:31:50 +08:00
|
|
|
Type: "SOA",
|
|
|
|
}
|
2018-02-16 01:02:50 +08:00
|
|
|
soaRec.SetLabel("@", origin)
|
2016-08-23 08:31:50 +08:00
|
|
|
if len(info.Ns) == 0 {
|
2017-07-20 03:53:40 +08:00
|
|
|
info.Ns = "DEFAULT_NOT_SET."
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
if len(info.Mbox) == 0 {
|
2017-07-20 03:53:40 +08:00
|
|
|
info.Mbox = "DEFAULT_NOT_SET."
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2018-02-16 01:02:50 +08:00
|
|
|
soaRec.SetTarget(info.String())
|
2016-08-23 08:31:50 +08:00
|
|
|
|
2017-03-23 00:38:08 +08:00
|
|
|
return &soaRec
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
|
2018-01-10 01:53:16 +08:00
|
|
|
// GetNameservers returns the nameservers for a domain.
|
2016-12-17 04:10:27 +08:00
|
|
|
func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
|
|
|
|
return c.nameservers, nil
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
|
2020-02-22 02:48:55 +08:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
filenames, err := filepath.Glob(filepath.Join(c.directory, "*.zone"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var zones []string
|
|
|
|
for _, n := range filenames {
|
|
|
|
_, file := filepath.Split(n)
|
|
|
|
zones = append(zones, strings.TrimSuffix(file, ".zone"))
|
|
|
|
}
|
|
|
|
return zones, nil
|
|
|
|
}
|
|
|
|
|
2020-02-18 21:59:18 +08:00
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
|
|
func (c *Bind) GetZoneRecords(domain string) (models.Records, error) {
|
2016-08-23 08:31:50 +08:00
|
|
|
|
2020-02-22 02:48:55 +08:00
|
|
|
// Default SOA record. If we see one in the zone, this will be replaced.
|
2020-02-18 21:59:18 +08:00
|
|
|
soaRec := makeDefaultSOA(c.DefaultSoa, domain)
|
|
|
|
foundRecords := models.Records{}
|
2017-03-23 00:38:08 +08:00
|
|
|
var oldSerial, newSerial uint32
|
2018-12-11 03:05:01 +08:00
|
|
|
|
|
|
|
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
|
|
|
fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
|
|
|
|
}
|
|
|
|
|
2020-02-18 21:59:18 +08:00
|
|
|
zonefile := filepath.Join(c.directory, strings.Replace(strings.ToLower(domain), "/", "_", -1)+".zone")
|
|
|
|
c.zonefile = zonefile
|
2017-03-23 00:38:08 +08:00
|
|
|
foundFH, err := os.Open(zonefile)
|
2020-02-18 21:59:18 +08:00
|
|
|
c.zoneFileFound = err == nil
|
2016-08-23 08:31:50 +08:00
|
|
|
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 {
|
2020-02-18 21:59:18 +08:00
|
|
|
for x := range dns.ParseZone(foundFH, domain, zonefile) {
|
2016-08-23 08:31:50 +08:00
|
|
|
if x.Error != nil {
|
|
|
|
log.Println("Error in zonefile:", x.Error)
|
|
|
|
} else {
|
2020-02-18 21:59:18 +08:00
|
|
|
rec, serial := models.RRtoRC(x.RR, domain, oldSerial)
|
2017-03-23 00:38:08 +08:00
|
|
|
if serial != 0 && oldSerial != 0 {
|
2016-08-23 08:31:50 +08:00
|
|
|
log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile)
|
|
|
|
}
|
|
|
|
if serial != 0 {
|
|
|
|
// This was an SOA record. Update the serial.
|
2017-03-23 00:38:08 +08:00
|
|
|
oldSerial = serial
|
2018-01-10 01:53:16 +08:00
|
|
|
newSerial = generateSerial(oldSerial)
|
2016-08-23 08:31:50 +08:00
|
|
|
// Regenerate with new serial:
|
2020-02-18 21:59:18 +08:00
|
|
|
*soaRec, _ = models.RRtoRC(x.RR, domain, newSerial)
|
2017-03-23 00:38:08 +08:00
|
|
|
rec = *soaRec
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
foundRecords = append(foundRecords, &rec)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-12 03:38:07 +08:00
|
|
|
// Add SOA record to expected set:
|
2020-02-18 21:59:18 +08:00
|
|
|
if !foundRecords.HasRecordTypeName("SOA", "@") {
|
|
|
|
//foundRecords = append(foundRecords, soaRec)
|
|
|
|
}
|
|
|
|
|
|
|
|
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 )
|
|
|
|
|
2020-02-23 02:27:24 +08:00
|
|
|
comments := make([]string, 0, 5)
|
|
|
|
|
|
|
|
comments = append(comments,
|
|
|
|
fmt.Sprintf("generated with dnscontrol %s", time.Now().Format(time.RFC3339)),
|
|
|
|
)
|
|
|
|
|
2020-02-18 21:59:18 +08:00
|
|
|
foundRecords, err := c.GetZoneRecords(dc.Name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
2020-02-23 02:27:24 +08:00
|
|
|
if dc.AutoDNSSEC {
|
|
|
|
comments = append(comments, "Automatic DNSSEC signing requested")
|
|
|
|
}
|
2016-08-23 08:31:50 +08:00
|
|
|
|
2017-11-08 06:12:17 +08:00
|
|
|
// Normalize
|
2018-01-05 08:19:35 +08:00
|
|
|
models.PostProcessRecords(foundRecords)
|
2017-11-08 06:12:17 +08:00
|
|
|
|
2017-01-12 03:38:07 +08:00
|
|
|
differ := diff.New(dc)
|
|
|
|
_, create, del, mod := differ.IncrementalDiff(foundRecords)
|
2016-08-23 08:31:50 +08:00
|
|
|
|
2017-03-15 05:47:40 +08:00
|
|
|
buf := &bytes.Buffer{}
|
2016-08-23 08:31:50 +08:00
|
|
|
// Print a list of changes. Generate an actual change that is the zone
|
|
|
|
changes := false
|
|
|
|
for _, i := range create {
|
|
|
|
changes = true
|
2020-02-18 21:59:18 +08:00
|
|
|
if c.zoneFileFound {
|
2017-03-15 05:47:40 +08:00
|
|
|
fmt.Fprintln(buf, i)
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, i := range del {
|
|
|
|
changes = true
|
2020-02-18 21:59:18 +08:00
|
|
|
if c.zoneFileFound {
|
2017-03-15 05:47:40 +08:00
|
|
|
fmt.Fprintln(buf, i)
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, i := range mod {
|
|
|
|
changes = true
|
2020-02-18 21:59:18 +08:00
|
|
|
if c.zoneFileFound {
|
2017-03-15 05:47:40 +08:00
|
|
|
fmt.Fprintln(buf, i)
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
}
|
2017-03-15 05:47:40 +08:00
|
|
|
msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name)
|
2020-02-18 21:59:18 +08:00
|
|
|
if !c.zoneFileFound {
|
2017-03-15 05:47:40 +08:00
|
|
|
msg = msg + fmt.Sprintf(" (%d records)\n", len(create))
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
2017-03-15 05:47:40 +08:00
|
|
|
msg += buf.String()
|
2016-08-23 08:31:50 +08:00
|
|
|
corrections := []*models.Correction{}
|
|
|
|
if changes {
|
|
|
|
corrections = append(corrections,
|
|
|
|
&models.Correction{
|
|
|
|
Msg: msg,
|
|
|
|
F: func() error {
|
2020-02-18 21:59:18 +08:00
|
|
|
fmt.Printf("CREATING ZONEFILE: %v\n", c.zonefile)
|
|
|
|
zf, err := os.Create(c.zonefile)
|
2016-08-23 08:31:50 +08:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Could not create zonefile: %v", err)
|
|
|
|
}
|
2020-02-23 02:27:24 +08:00
|
|
|
// Beware that if there are any fake types, then they will
|
|
|
|
// be commented out on write, but we don't reverse that when
|
|
|
|
// reading, so there will be a diff on every invocation.
|
|
|
|
err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name, comments)
|
2016-08-23 08:31:50 +08:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("WriteZoneFile error: %v\n", err)
|
|
|
|
}
|
|
|
|
err = zf.Close()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Closing: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return corrections, nil
|
|
|
|
}
|