mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-11-14 12:15:06 +08:00
460 lines
13 KiB
Go
460 lines
13 KiB
Go
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/diff2"
|
|
"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())
|
|
if c.transferKey.algo == dns.HmacMD5 {
|
|
transfer.TsigProvider = md5Provider(c.transferKey.secret)
|
|
}
|
|
}
|
|
|
|
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: %s", domain, 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
|
|
|
|
var corrections []*models.Correction
|
|
var create, del, mod diff.Changeset
|
|
if !diff2.EnableDiff2 {
|
|
differ := diff.New(dc)
|
|
_, create, del, mod, err = differ.IncrementalDiff(foundRecords)
|
|
} else {
|
|
differ := diff.NewCompat(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)
|
|
|
|
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())
|
|
if c.updateKey.algo == dns.HmacMD5 {
|
|
client.TsigProvider = md5Provider(c.updateKey.secret)
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
}
|