package axfrddns /* axfrddns - Fetch the zone with an AXFR request (RFC5936) to a given primary master, and push Dynamic DNS updates (RFC2136) to the same server. Both the AXFR request and the updates might be authentificated with a TSIG. */ import ( "bytes" "crypto/tls" "encoding/base64" "encoding/json" "fmt" "math" "math/rand" "net" "strings" "time" "github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/printer" "github.com/StackExchange/dnscontrol/v3/pkg/txtutil" "github.com/StackExchange/dnscontrol/v3/providers" "github.com/miekg/dns" ) const ( dnsTimeout = 30 * time.Second dnssecDummyLabel = "__dnssec" dnssecDummyTxt = "Domain has DNSSec records, not displayed here." ) var features = providers.DocumentationNotes{ providers.CanAutoDNSSEC: providers.Can("Just warn when DNSSEC is requested but no RRSIG is found in the AXFR or warn when DNSSEC is not requested but RRSIG are found in the AXFR."), providers.CanGetZones: providers.Cannot(), providers.CanUseCAA: providers.Can(), providers.CanUseNAPTR: providers.Can(), providers.CanUsePTR: providers.Can(), providers.CanUseSRV: providers.Can(), providers.CanUseSSHFP: providers.Can(), providers.CanUseTLSA: providers.Can(), providers.CantUseNOPURGE: providers.Cannot(), providers.DocCreateDomains: providers.Cannot(), providers.DocDualHost: providers.Cannot(), providers.DocOfficiallySupported: providers.Cannot(), } // axfrddnsProvider stores the client info for the provider. type axfrddnsProvider struct { rand *rand.Rand master string updateMode string transferMode string nameservers []*models.Nameserver transferKey *Key updateKey *Key } func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) { // config -- the key/values from creds.json // providermeta -- the json blob from NewReq('name', 'TYPE', providermeta) var err error api := &axfrddnsProvider{ rand: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))), } param := &Param{} if len(providermeta) != 0 { err := json.Unmarshal(providermeta, param) if err != nil { return nil, err } } var nss []string if config["nameservers"] != "" { nss = strings.Split(config["nameservers"], ",") } for _, ns := range param.DefaultNS { nss = append(nss, ns[0:len(ns)-1]) } api.nameservers, err = models.ToNameservers(nss) if err != nil { return nil, err } if config["update-mode"] != "" { switch config["update-mode"] { case "tcp", "tcp-tls": api.updateMode = config["update-mode"] case "udp": api.updateMode = "" default: printer.Printf("[Warning] AXFRDDNS: Unknown update-mode in `creds.json` (%s)\n", config["update-mode"]) } } else { api.updateMode = "" } if config["transfer-mode"] != "" { switch config["transfer-mode"] { case "tcp", "tcp-tls": api.transferMode = config["transfer-mode"] default: printer.Printf("[Warning] AXFRDDNS: Unknown transfer-mode in `creds.json` (%s)\n", config["transfer-mode"]) } } else { api.transferMode = "tcp" } if config["master"] != "" { api.master = config["master"] if !strings.Contains(api.master, ":") { api.master = api.master + ":53" } } else if len(api.nameservers) != 0 { api.master = api.nameservers[0].Name + ":53" } else { return nil, fmt.Errorf("nameservers list is empty: creds.json needs a default `nameservers` or an explicit `master`") } api.updateKey, err = readKey(config["update-key"], "update-key") if err != nil { return nil, err } api.transferKey, err = readKey(config["transfer-key"], "transfer-key") if err != nil { return nil, err } for key := range config { switch key { case "master", "nameservers", "update-key", "transfer-key", "update-mode", "transfer-mode": continue default: printer.Printf("[Warning] AXFRDDNS: unknown key in `creds.json` (%s)\n", key) } } return api, err } func init() { fns := providers.DspFuncs{ Initializer: initAxfrDdns, RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType("AXFRDDNS", fns, features) } // Param is used to decode extra parameters sent to provider. type Param struct { DefaultNS []string `json:"default_ns"` } // Key stores the individual parts of a TSIG key. type Key struct { algo string id string secret string } func readKey(raw string, kind string) (*Key, error) { if raw == "" { return nil, nil } arr := strings.Split(raw, ":") if len(arr) != 3 { return nil, fmt.Errorf("invalid key format (%s) in AXFRDDNS.TSIG", kind) } var algo string switch arr[0] { case "hmac-md5", "md5": algo = dns.HmacMD5 case "hmac-sha1", "sha1": algo = dns.HmacSHA1 case "hmac-sha256", "sha256": algo = dns.HmacSHA256 case "hmac-sha512", "sha512": algo = dns.HmacSHA512 default: return nil, fmt.Errorf("unknown algorithm (%s) in AXFRDDNS.TSIG", kind) } _, err := base64.StdEncoding.DecodeString(arr[2]) if err != nil { return nil, fmt.Errorf("cannot decode Base64 secret (%s) in AXFRDDNS.TSIG", kind) } return &Key{algo: algo, id: arr[1] + ".", secret: arr[2]}, nil } // GetNameservers returns the nameservers for a domain. func (c *axfrddnsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) { return c.nameservers, nil } func (c *axfrddnsProvider) getAxfrConnection() (*dns.Transfer, error) { var con net.Conn = nil var err error = nil if c.transferMode == "tcp-tls" { con, err = tls.Dial("tcp", c.master, &tls.Config{}) } else { con, err = net.Dial("tcp", c.master) } if err != nil { return nil, err } dnscon := &dns.Conn{Conn: con} transfer := &dns.Transfer{Conn: dnscon} return transfer, nil } // FetchZoneRecords gets the records of a zone and returns them in dns.RR format. func (c *axfrddnsProvider) FetchZoneRecords(domain string) ([]dns.RR, error) { transfer, err := c.getAxfrConnection() if err != nil { return nil, err } transfer.DialTimeout = dnsTimeout transfer.ReadTimeout = dnsTimeout request := new(dns.Msg) request.SetAxfr(domain + ".") if c.transferKey != nil { transfer.TsigSecret = map[string]string{c.transferKey.id: c.transferKey.secret} request.SetTsig(c.transferKey.id, c.transferKey.algo, 300, time.Now().Unix()) } envelope, err := transfer.In(request, c.master) if err != nil { return nil, err } var rawRecords []dns.RR for msg := range envelope { if msg.Error != nil { // Fragile but more "user-friendly" error-handling err := msg.Error.Error() if err == "dns: bad xfr rcode: 9" { err = "NOT AUTH (9)" } return nil, fmt.Errorf("[Error] AXFRDDNS: nameserver refused to transfer the zone: %s", err) } rawRecords = append(rawRecords, msg.RR...) } return rawRecords, nil } // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. func (c *axfrddnsProvider) GetZoneRecords(domain string) (models.Records, error) { rawRecords, err := c.FetchZoneRecords(domain) if err != nil { return nil, err } var foundDNSSecRecords *models.RecordConfig foundRecords := models.Records{} for _, rr := range rawRecords { switch rr.(type) { case *dns.RRSIG, *dns.DNSKEY, *dns.CDNSKEY, *dns.CDS, *dns.NSEC, *dns.NSEC3, *dns.NSEC3PARAM: // Ignoring DNSSec RRs, but replacing it with a single // "TXT" placeholder if foundDNSSecRecords == nil { foundDNSSecRecords = new(models.RecordConfig) foundDNSSecRecords.Type = "TXT" foundDNSSecRecords.SetLabel(dnssecDummyLabel, domain) err = foundDNSSecRecords.SetTargetTXT(dnssecDummyTxt) if err != nil { return nil, err } } continue default: rec, err := models.RRtoRC(rr, domain) if err != nil { return nil, err } foundRecords = append(foundRecords, &rec) } } if len(foundRecords) >= 1 && foundRecords[len(foundRecords)-1].Type == "SOA" { // The SOA is sent two times: as the first and the last record // See section 2.2 of RFC5936 foundRecords = foundRecords[:len(foundRecords)-1] } if foundDNSSecRecords != nil { foundRecords = append(foundRecords, foundDNSSecRecords) } return foundRecords, nil } // GetDomainCorrections returns a list of corrections to update a domain. func (c *axfrddnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { dc.Punycode() foundRecords, err := c.GetZoneRecords(dc.Name) if err != nil { return nil, err } if len(foundRecords) >= 1 && foundRecords[0].Type == "SOA" { // Ignoring the SOA, others providers don't manage it either. foundRecords = foundRecords[1:] } hasDnssecRecords := false if len(foundRecords) >= 1 { last := foundRecords[len(foundRecords)-1] if last.Type == "TXT" && last.Name == dnssecDummyLabel && len(last.TxtStrings) == 1 && last.TxtStrings[0] == dnssecDummyTxt { hasDnssecRecords = true foundRecords = foundRecords[0:(len(foundRecords) - 1)] } } // TODO(tlim): This check should be done on all providers. Move to the global validation code. if dc.AutoDNSSEC == "on" && !hasDnssecRecords { printer.Printf("Warning: AUTODNSSEC is enabled, but no DNSKEY or RRSIG record was found in the AXFR answer!\n") } if dc.AutoDNSSEC == "off" && hasDnssecRecords { printer.Printf("Warning: AUTODNSSEC is disabled, but DNSKEY or RRSIG records were found in the AXFR answer!\n") } // Normalize models.PostProcessRecords(foundRecords) txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records differ := diff.New(dc) _, create, del, mod, err := differ.IncrementalDiff(foundRecords) if err != nil { return nil, err } buf := &bytes.Buffer{} // Print a list of changes. Generate an actual change that is the zone changes := false for _, i := range create { changes = true fmt.Fprintln(buf, i) } for _, i := range del { changes = true fmt.Fprintln(buf, i) } for _, i := range mod { changes = true fmt.Fprintln(buf, i) } msg := fmt.Sprintf("DDNS UPDATES to '%s' (primary master: '%s'). Changes:\n%s", dc.Name, c.master, buf) corrections := []*models.Correction{} if changes { corrections = append(corrections, &models.Correction{ Msg: msg, F: func() error { // An RFC2136-compliant server must silently ignore an // update that inserts a non-CNAME RRset when a CNAME RR // with the same name is present in the zone (and // vice-versa). Therefore we prefer to first remove records // and then insert new ones. // // Compliant servers must also silently ignore an update // that removes the last NS record of a zone. Therefore we // don't want to remove all NS records before inserting a // new one. For the particular case of NS record, we prefer // to insert new records before ot remove old ones. // // This remarks does not apply for "modified" NS records, as // updates are processed one-by-one. // // This provider does not allow modifying the TTL of an NS // record in a zone that defines only one NS. That would // would require removing the single NS record, before // adding the new one. But who does that anyway? update := new(dns.Msg) update.SetUpdate(dc.Name + ".") update.Id = uint16(c.rand.Intn(math.MaxUint16)) for _, c := range create { if c.Desired.Type == "NS" { update.Insert([]dns.RR{c.Desired.ToRR()}) } } for _, c := range del { update.Remove([]dns.RR{c.Existing.ToRR()}) } for _, c := range mod { update.Remove([]dns.RR{c.Existing.ToRR()}) update.Insert([]dns.RR{c.Desired.ToRR()}) } for _, c := range create { if c.Desired.Type != "NS" { update.Insert([]dns.RR{c.Desired.ToRR()}) } } client := new(dns.Client) client.Net = c.updateMode client.Timeout = dnsTimeout if c.updateKey != nil { client.TsigSecret = map[string]string{c.updateKey.id: c.updateKey.secret} update.SetTsig(c.updateKey.id, c.updateKey.algo, 300, time.Now().Unix()) } msg, _, err := client.Exchange(update, c.master) if err != nil { return err } if msg.MsgHdr.Rcode != 0 { return fmt.Errorf("[Error] AXFRDDNS: nameserver refused to update the zone: %s (%d)", dns.RcodeToString[msg.MsgHdr.Rcode], msg.MsgHdr.Rcode) } return nil }, }) } return corrections, nil }