2020-05-08 22:55:51 +08: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 (
"bytes"
2021-05-07 20:21:14 +08:00
"crypto/tls"
2020-05-08 22:55:51 +08:00
"encoding/base64"
"encoding/json"
"fmt"
"math"
"math/rand"
2021-05-07 20:21:14 +08:00
"net"
2020-05-08 22:55:51 +08:00
"strings"
"time"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
2022-12-12 04:02:58 +08:00
"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
2022-08-15 08:46:56 +08:00
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
2021-03-08 02:19:22 +08:00
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
2020-05-08 22:55:51 +08:00
"github.com/StackExchange/dnscontrol/v3/providers"
2022-08-15 08:46:56 +08:00
"github.com/miekg/dns"
2020-05-08 22:55:51 +08:00
)
const (
dnsTimeout = 30 * time . Second
dnssecDummyLabel = "__dnssec"
dnssecDummyTxt = "Domain has DNSSec records, not displayed here."
)
var features = providers . DocumentationNotes {
2022-03-03 00:19:15 +08: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 22:55:51 +08:00
providers . CanUseCAA : providers . Can ( ) ,
providers . CanUseNAPTR : providers . Can ( ) ,
2022-03-03 00:19:15 +08:00
providers . CanUsePTR : providers . Can ( ) ,
2020-05-08 22:55:51 +08:00
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 ( ) ,
}
2020-10-26 21:25:30 +08:00
// axfrddnsProvider stores the client info for the provider.
type axfrddnsProvider struct {
2021-05-07 20:21:14 +08:00
rand * rand . Rand
master string
updateMode string
transferMode string
nameservers [ ] * models . Nameserver
transferKey * Key
updateKey * Key
2020-05-08 22:55:51 +08: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 21:25:30 +08:00
api := & axfrddnsProvider {
2020-05-08 22:55:51 +08: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 20:21:14 +08: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 21:01:02 +08:00
printer . Printf ( "[Warning] AXFRDDNS: Unknown update-mode in `creds.json` (%s)\n" , config [ "update-mode" ] )
2021-05-07 20:21:14 +08: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 21:01:02 +08:00
printer . Printf ( "[Warning] AXFRDDNS: Unknown transfer-mode in `creds.json` (%s)\n" , config [ "transfer-mode" ] )
2021-05-07 20:21:14 +08:00
}
} else {
api . transferMode = "tcp"
}
2020-05-08 22:55:51 +08: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
}
for key := range config {
switch key {
case "master" ,
"nameservers" ,
"update-key" ,
2021-05-07 20:21:14 +08:00
"transfer-key" ,
"update-mode" ,
"transfer-mode" :
2020-05-08 22:55:51 +08:00
continue
default :
2022-06-18 21:01:02 +08:00
printer . Printf ( "[Warning] AXFRDDNS: unknown key in `creds.json` (%s)\n" , key )
2020-05-08 22:55:51 +08:00
}
}
return api , err
}
func init ( ) {
2021-03-08 02:19:22 +08:00
fns := providers . DspFuncs {
2021-05-05 02:15:31 +08:00
Initializer : initAxfrDdns ,
2021-03-09 09:14:30 +08:00
RecordAuditor : AuditRecords ,
2021-03-08 02:19:22 +08:00
}
providers . RegisterDomainServiceProviderType ( "AXFRDDNS" , fns , features )
2020-05-08 22:55:51 +08: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 21:25:30 +08:00
func ( c * axfrddnsProvider ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
2020-05-08 22:55:51 +08:00
return c . nameservers , nil
}
2021-05-07 20:21:14 +08: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 22:55:51 +08:00
// FetchZoneRecords gets the records of a zone and returns them in dns.RR format.
2020-10-26 21:25:30 +08:00
func ( c * axfrddnsProvider ) FetchZoneRecords ( domain string ) ( [ ] dns . RR , error ) {
2021-05-07 20:21:14 +08:00
transfer , err := c . getAxfrConnection ( )
if err != nil {
return nil , err
}
2020-05-08 22:55:51 +08: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 19:13:44 +08:00
if c . transferKey . algo == dns . HmacMD5 {
transfer . TsigProvider = md5Provider ( c . transferKey . secret )
}
2020-05-08 22:55:51 +08: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)"
}
2020-09-24 23:01:26 +08:00
return nil , fmt . Errorf ( "[Error] AXFRDDNS: nameserver refused to transfer the zone: %s" , err )
2020-05-08 22:55:51 +08:00
}
rawRecords = append ( rawRecords , msg . RR ... )
}
return rawRecords , nil
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
2020-10-26 21:25:30 +08:00
func ( c * axfrddnsProvider ) GetZoneRecords ( domain string ) ( models . Records , error ) {
2020-05-08 22:55:51 +08:00
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 :
2021-07-06 23:03:29 +08:00
rec , err := models . RRtoRC ( rr , domain )
if err != nil {
return nil , err
}
2020-05-08 22:55:51 +08: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
// 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.
2020-10-26 21:25:30 +08:00
func ( c * axfrddnsProvider ) GetDomainCorrections ( dc * models . DomainConfig ) ( [ ] * models . Correction , error ) {
2020-05-08 22:55:51 +08:00
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 ) ]
}
}
2020-09-28 04:37:42 +08:00
// TODO(tlim): This check should be done on all providers. Move to the global validation code.
if dc . AutoDNSSEC == "on" && ! hasDnssecRecords {
2022-06-18 21:01:02 +08:00
printer . Printf ( "Warning: AUTODNSSEC is enabled, but no DNSKEY or RRSIG record was found in the AXFR answer!\n" )
2020-09-28 04:37:42 +08:00
}
if dc . AutoDNSSEC == "off" && hasDnssecRecords {
2022-06-18 21:01:02 +08:00
printer . Printf ( "Warning: AUTODNSSEC is disabled, but DNSKEY or RRSIG records were found in the AXFR answer!\n" )
2020-05-08 22:55:51 +08:00
}
// Normalize
models . PostProcessRecords ( foundRecords )
2021-03-08 02:19:22 +08:00
txtutil . SplitSingleLongTxt ( dc . Records ) // Autosplit long TXT records
2020-05-08 22:55:51 +08:00
2022-12-12 04:02:58 +08:00
var corrections [ ] * models . Correction
if ! diff2 . EnableDiff2 || true { // Remove "|| true" when diff2 version arrives
2020-05-08 22:55:51 +08:00
2022-12-12 04:02:58 +08:00
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 )
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 ( ) } )
}
2020-05-08 22:55:51 +08:00
}
2022-12-12 04:02:58 +08:00
for _ , c := range del {
update . Remove ( [ ] dns . RR { c . Existing . ToRR ( ) } )
}
for _ , c := range mod {
update . Remove ( [ ] dns . RR { c . Existing . ToRR ( ) } )
2020-05-08 22:55:51 +08:00
update . Insert ( [ ] dns . RR { c . Desired . ToRR ( ) } )
}
2022-12-12 04:02:58 +08:00
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 ( ) )
2022-12-31 19:13:44 +08:00
if c . updateKey . algo == dns . HmacMD5 {
client . TsigProvider = md5Provider ( c . updateKey . secret )
}
2022-12-12 04:02:58 +08: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
} ,
} )
}
return corrections , nil
2020-05-08 22:55:51 +08:00
}
2022-12-12 04:02:58 +08:00
// Insert Future diff2 version here.
2020-05-08 22:55:51 +08:00
return corrections , nil
2022-12-12 06:28:58 +08:00
2020-05-08 22:55:51 +08:00
}