2020-05-08 16:55:51 +02:00
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 (
2021-05-07 14:21:14 +02:00
"crypto/tls"
2020-05-08 16:55:51 +02:00
"encoding/base64"
"encoding/json"
"fmt"
"math"
"math/rand"
2021-05-07 14:21:14 +02:00
"net"
2020-05-08 16:55:51 +02:00
"strings"
"time"
2023-05-20 19:21:45 +02:00
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
2023-04-14 15:22:23 -04:00
"github.com/fatih/color"
2022-08-14 20:46:56 -04:00
"github.com/miekg/dns"
2020-05-08 16:55:51 +02:00
)
const (
dnsTimeout = 30 * time . Second
dnssecDummyLabel = "__dnssec"
dnssecDummyTxt = "Domain has DNSSec records, not displayed here."
)
var features = providers . DocumentationNotes {
2022-03-02 11:19:15 -05:00
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 ( ) ,
2020-05-08 16:55:51 +02:00
providers . CanUseCAA : providers . Can ( ) ,
2023-03-16 19:59:44 -04:00
providers . CanUseLOC : providers . Unimplemented ( ) ,
2020-05-08 16:55:51 +02:00
providers . CanUseNAPTR : providers . Can ( ) ,
2022-03-02 11:19:15 -05:00
providers . CanUsePTR : providers . Can ( ) ,
2020-05-08 16:55:51 +02:00
providers . CanUseSRV : providers . Can ( ) ,
providers . CanUseSSHFP : providers . Can ( ) ,
providers . CanUseTLSA : providers . Can ( ) ,
2023-06-02 06:10:35 +02:00
providers . CanUseDHCID : providers . Can ( ) ,
2020-05-08 16:55:51 +02:00
providers . CantUseNOPURGE : providers . Cannot ( ) ,
providers . DocCreateDomains : providers . Cannot ( ) ,
providers . DocDualHost : providers . Cannot ( ) ,
providers . DocOfficiallySupported : providers . Cannot ( ) ,
}
2020-10-26 09:25:30 -04:00
// axfrddnsProvider stores the client info for the provider.
type axfrddnsProvider struct {
2023-04-14 15:22:23 -04:00
rand * rand . Rand
master string
updateMode string
transferMode string
nameservers [ ] * models . Nameserver
transferKey * Key
updateKey * Key
hasDnssecRecords bool
serverHasBuggyCNAME bool
2020-05-08 16:55:51 +02:00
}
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
2020-10-26 09:25:30 -04:00
api := & axfrddnsProvider {
2020-05-08 16:55:51 +02:00
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
}
2021-05-07 14:21:14 +02:00
if config [ "update-mode" ] != "" {
switch config [ "update-mode" ] {
case "tcp" ,
"tcp-tls" :
api . updateMode = config [ "update-mode" ]
case "udp" :
api . updateMode = ""
default :
2022-06-18 15:01:02 +02:00
printer . Printf ( "[Warning] AXFRDDNS: Unknown update-mode in `creds.json` (%s)\n" , config [ "update-mode" ] )
2021-05-07 14:21:14 +02:00
}
} else {
api . updateMode = ""
}
if config [ "transfer-mode" ] != "" {
switch config [ "transfer-mode" ] {
case "tcp" ,
"tcp-tls" :
api . transferMode = config [ "transfer-mode" ]
default :
2022-06-18 15:01:02 +02:00
printer . Printf ( "[Warning] AXFRDDNS: Unknown transfer-mode in `creds.json` (%s)\n" , config [ "transfer-mode" ] )
2021-05-07 14:21:14 +02:00
}
} else {
api . transferMode = "tcp"
}
2020-05-08 16:55:51 +02:00
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
}
2023-04-14 15:22:23 -04:00
switch strings . ToLower ( strings . TrimSpace ( config [ "buggy-cname" ] ) ) {
case "yes" , "true" :
api . serverHasBuggyCNAME = true
default :
api . serverHasBuggyCNAME = false
}
2020-05-08 16:55:51 +02:00
for key := range config {
switch key {
case "master" ,
"nameservers" ,
"update-key" ,
2021-05-07 14:21:14 +02:00
"transfer-key" ,
"update-mode" ,
2023-04-14 15:22:23 -04:00
"transfer-mode" ,
"domain" ,
"TYPE" :
2020-05-08 16:55:51 +02:00
continue
default :
2022-06-18 15:01:02 +02:00
printer . Printf ( "[Warning] AXFRDDNS: unknown key in `creds.json` (%s)\n" , key )
2020-05-08 16:55:51 +02:00
}
}
return api , err
}
func init ( ) {
2021-03-07 13:19:22 -05:00
fns := providers . DspFuncs {
2021-05-04 14:15:31 -04:00
Initializer : initAxfrDdns ,
2021-03-08 20:14:30 -05:00
RecordAuditor : AuditRecords ,
2021-03-07 13:19:22 -05:00
}
providers . RegisterDomainServiceProviderType ( "AXFRDDNS" , fns , features )
2020-05-08 16:55:51 +02:00
}
// 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.
2020-10-26 09:25:30 -04:00
func ( c * axfrddnsProvider ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
2020-05-08 16:55:51 +02:00
return c . nameservers , nil
}
2021-05-07 14:21:14 +02:00
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
}
2020-05-08 16:55:51 +02:00
// FetchZoneRecords gets the records of a zone and returns them in dns.RR format.
2020-10-26 09:25:30 -04:00
func ( c * axfrddnsProvider ) FetchZoneRecords ( domain string ) ( [ ] dns . RR , error ) {
2021-05-07 14:21:14 +02:00
transfer , err := c . getAxfrConnection ( )
if err != nil {
return nil , err
}
2020-05-08 16:55:51 +02:00
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 ( ) )
2022-12-31 12:13:44 +01:00
if c . transferKey . algo == dns . HmacMD5 {
transfer . TsigProvider = md5Provider ( c . transferKey . secret )
}
2020-05-08 16:55:51 +02:00
}
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)"
}
2023-02-19 16:54:53 +01:00
return nil , fmt . Errorf ( "[Error] AXFRDDNS: nameserver refused to transfer the zone %s: %s" , domain , err )
2020-05-08 16:55:51 +02:00
}
rawRecords = append ( rawRecords , msg . RR ... )
}
return rawRecords , nil
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
2023-05-02 13:04:59 -04:00
func ( c * axfrddnsProvider ) GetZoneRecords ( domain string , meta map [ string ] string ) ( models . Records , error ) {
2020-05-08 16:55:51 +02:00
rawRecords , err := c . FetchZoneRecords ( domain )
if err != nil {
return nil , err
}
var foundDNSSecRecords * models . RecordConfig
foundRecords := models . Records { }
for _ , rr := range rawRecords {
2023-05-09 03:44:42 +02:00
switch rr . Header ( ) . Rrtype {
case dns . TypeRRSIG ,
dns . TypeDNSKEY ,
dns . TypeCDNSKEY ,
dns . TypeCDS ,
dns . TypeNSEC ,
dns . TypeNSEC3 ,
dns . TypeNSEC3PARAM ,
65534 :
2020-05-08 16:55:51 +02:00
// Ignoring DNSSec RRs, but replacing it with a single
// "TXT" placeholder
2023-05-09 03:44:42 +02:00
// Also ignoring spurious TYPE65534, see:
// https://bind9-users.isc.narkive.com/zX29ay0j/rndc-signing-list-not-working#post2
2020-05-08 16:55:51 +02:00
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 :
2021-07-06 17:03:29 +02:00
rec , err := models . RRtoRC ( rr , domain )
if err != nil {
return nil , err
}
2020-05-08 16:55:51 +02:00
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
2023-04-14 15:22:23 -04:00
// See section 2.2 of RFC5936. We remove the later one.
2020-05-08 16:55:51 +02:00
foundRecords = foundRecords [ : len ( foundRecords ) - 1 ]
}
if foundDNSSecRecords != nil {
foundRecords = append ( foundRecords , foundDNSSecRecords )
}
2023-04-14 15:22:23 -04:00
c . 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 {
c . hasDnssecRecords = true
foundRecords = foundRecords [ 0 : ( len ( foundRecords ) - 1 ) ]
}
}
2020-05-08 16:55:51 +02:00
return foundRecords , nil
}
2023-04-14 15:22:23 -04:00
// BuildCorrection return a Correction for a given set of DDNS update and the corresponding message.
func ( c * axfrddnsProvider ) BuildCorrection ( dc * models . DomainConfig , msgs [ ] string , update * dns . Msg ) * models . Correction {
2023-05-18 21:14:12 +02:00
if update == nil {
return & models . Correction {
Msg : fmt . Sprintf ( "DDNS UPDATES to '%s' (primary master: '%s'). Changes:\n%s" , dc . Name , c . master , strings . Join ( msgs , "\n" ) ) ,
}
}
2023-04-14 15:22:23 -04:00
return & models . Correction {
Msg : fmt . Sprintf ( "DDNS UPDATES to '%s' (primary master: '%s'). Changes:\n%s" , dc . Name , c . master , strings . Join ( msgs , "\n" ) ) ,
F : func ( ) error {
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 )
}
}
2020-05-08 16:55:51 +02:00
2023-04-14 15:22:23 -04:00
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
} ,
2020-05-08 16:55:51 +02:00
}
2023-04-14 15:22:23 -04:00
}
2020-05-08 16:55:51 +02:00
2023-04-14 15:22:23 -04:00
// hasDeletionForName returns true if there exist a corrections for [name] which is a deletion
func hasDeletionForName ( changes diff2 . ChangeList , name string ) bool {
for _ , change := range changes {
switch change . Type {
case diff2 . DELETE :
if change . Old [ 0 ] . Name == name {
return true
}
}
2020-05-08 16:55:51 +02:00
}
2023-04-14 15:22:23 -04:00
return false
}
2020-05-08 16:55:51 +02:00
2023-04-14 15:22:23 -04:00
// hasNSDeletion returns true if there exist a correction that deletes or changes an NS record
func hasNSDeletion ( changes diff2 . ChangeList ) bool {
for _ , change := range changes {
switch change . Type {
case diff2 . CHANGE :
if change . Old [ 0 ] . Type == "NS" && change . Old [ 0 ] . Name == "@" {
return true
}
case diff2 . DELETE :
if change . Old [ 0 ] . Type == "NS" && change . Old [ 0 ] . Name == "@" {
return true
}
case diff2 . CREATE :
case diff2 . REPORT :
2020-05-08 16:55:51 +02:00
}
}
2023-04-14 15:22:23 -04:00
return false
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func ( c * axfrddnsProvider ) GetZoneRecordsCorrections ( dc * models . DomainConfig , foundRecords models . Records ) ( [ ] * models . Correction , error ) {
txtutil . SplitSingleLongTxt ( foundRecords ) // Autosplit long TXT records
// Ignoring the SOA, others providers don't manage it either.
if len ( foundRecords ) >= 1 && foundRecords [ 0 ] . Type == "SOA" {
foundRecords = foundRecords [ 1 : ]
}
2020-05-08 16:55:51 +02:00
2020-09-27 16:37:42 -04:00
// TODO(tlim): This check should be done on all providers. Move to the global validation code.
2023-04-14 15:22:23 -04:00
if dc . AutoDNSSEC == "on" && ! c . hasDnssecRecords {
2022-06-18 15:01:02 +02:00
printer . Printf ( "Warning: AUTODNSSEC is enabled, but no DNSKEY or RRSIG record was found in the AXFR answer!\n" )
2020-09-27 16:37:42 -04:00
}
2023-04-14 15:22:23 -04:00
if dc . AutoDNSSEC == "off" && c . hasDnssecRecords {
2022-06-18 15:01:02 +02:00
printer . Printf ( "Warning: AUTODNSSEC is disabled, but DNSKEY or RRSIG records were found in the AXFR answer!\n" )
2020-05-08 16:55:51 +02:00
}
2023-04-14 15:22:23 -04:00
// 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. Then, when an update want to change a NS record,
// we first insert a dummy NS record that we will remove
// at the end of the batched update.
var msgs [ ] string
2023-05-18 21:14:12 +02:00
var reports [ ] string
2023-04-14 15:22:23 -04:00
var msgs2 [ ] string
update := new ( dns . Msg )
update . SetUpdate ( dc . Name + "." )
update . Id = uint16 ( c . rand . Intn ( math . MaxUint16 ) )
update2 := new ( dns . Msg )
update2 . SetUpdate ( dc . Name + "." )
update2 . Id = uint16 ( c . rand . Intn ( math . MaxUint16 ) )
hasTwoCorrections := false
dummyNs1 , err := dns . NewRR ( dc . Name + ". IN NS 255.255.255.255" )
if err != nil {
return nil , err
2023-01-03 13:06:21 -05:00
}
2023-04-14 15:22:23 -04:00
dummyNs2 , err := dns . NewRR ( dc . Name + ". IN NS 255.255.255.255" )
2023-01-03 13:06:21 -05:00
if err != nil {
return nil , err
}
2022-12-11 15:02:58 -05:00
2023-04-14 15:22:23 -04:00
changes , err := diff2 . ByRecord ( foundRecords , dc , nil )
if err != nil {
return nil , err
}
if changes == nil {
return nil , nil
}
// A DNS server should silently ignore a DDNS update that removes
// the last NS record of a zone. Since modifying a record is
// implemented by successively a deletion of the old record and an
// insertion of the new one, then modifying all the NS record of a
// zone might will fail (even if the the deletion and insertion
// are grouped in a single batched update).
//
// To avoid this case, we will first insert a dummy NS record,
// that will be removed at the end of the batched updates. This
// record needs to inserted only when all NS records are touched
// The current implementation insert this dummy record as soon as
// a NS record is deleted or changed.
hasNSDeletion := hasNSDeletion ( changes )
if hasNSDeletion {
update . Insert ( [ ] dns . RR { dummyNs1 } )
}
for _ , change := range changes {
switch change . Type {
case diff2 . DELETE :
msgs = append ( msgs , change . Msgs [ 0 ] )
update . Remove ( [ ] dns . RR { change . Old [ 0 ] . ToRR ( ) } )
case diff2 . CREATE :
if c . serverHasBuggyCNAME &&
change . New [ 0 ] . Type == "CNAME" &&
hasDeletionForName ( changes , change . New [ 0 ] . Name ) {
hasTwoCorrections = true
msgs2 = append ( msgs2 , change . Msgs [ 0 ] )
update2 . Insert ( [ ] dns . RR { change . New [ 0 ] . ToRR ( ) } )
} else {
msgs = append ( msgs , change . Msgs [ 0 ] )
update . Insert ( [ ] dns . RR { change . New [ 0 ] . ToRR ( ) } )
}
case diff2 . CHANGE :
if c . serverHasBuggyCNAME && change . New [ 0 ] . Type == "CNAME" {
msgs = append ( msgs , change . Msgs [ 0 ] + color . RedString ( " (delete)" ) )
update . Remove ( [ ] dns . RR { change . Old [ 0 ] . ToRR ( ) } )
hasTwoCorrections = true
msgs2 = append ( msgs2 , change . Msgs [ 0 ] + color . GreenString ( " (create)" ) )
update2 . Insert ( [ ] dns . RR { change . New [ 0 ] . ToRR ( ) } )
} else {
msgs = append ( msgs , change . Msgs [ 0 ] )
update . Remove ( [ ] dns . RR { change . Old [ 0 ] . ToRR ( ) } )
update . Insert ( [ ] dns . RR { change . New [ 0 ] . ToRR ( ) } )
}
case diff2 . REPORT :
2023-05-18 21:14:12 +02:00
reports = append ( reports , change . Msgs ... )
2023-04-14 15:22:23 -04:00
}
2020-05-08 16:55:51 +02:00
}
2022-12-11 15:02:58 -05:00
2023-04-14 15:22:23 -04:00
if hasNSDeletion {
update . Remove ( [ ] dns . RR { dummyNs2 } )
}
2023-05-18 21:14:12 +02:00
returnValue := [ ] * models . Correction { }
2022-12-11 17:28:58 -05:00
2023-05-18 21:14:12 +02:00
if len ( msgs ) > 0 {
returnValue = append ( returnValue , c . BuildCorrection ( dc , msgs , update ) )
}
if hasTwoCorrections && len ( msgs2 ) > 0 {
returnValue = append ( returnValue , c . BuildCorrection ( dc , msgs2 , update2 ) )
}
if len ( reports ) > 0 {
returnValue = append ( returnValue , c . BuildCorrection ( dc , reports , nil ) )
}
return returnValue , nil
2020-05-08 16:55:51 +02:00
}