New provider: AXFR+DDNS (#259) (#729)

* NEW PROVIDER: AXFR+DDNS (#259)

* AXFRDDNS: split GetZoneRecords in two functions

* AXFRDDNS: improve code documentation

* AXFRDDNS: line-wrap documentation

* AXFRDDNS: add simple `named.conf` as example

* AXFRDDNS: improve error messages

* AXFRDDNS: improve doc.

* AXFRDDNS: update `OWNERS`

* Linting and other cosmetic changes

* AXFRDDNS: fix grammar

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Grégoire Henry 2020-05-08 16:55:51 +02:00 committed by GitHub
parent ec27d2c76a
commit 8dd66ec605
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 639 additions and 0 deletions

1
OWNERS
View file

@ -1,4 +1,5 @@
# providers/activedir
providers/axfrddns @hnrgrgr
providers/azuredns @vatsalyagoel
providers/bind @tlimoncelli
# providers/cloudflare

View file

@ -16,6 +16,7 @@ Windows). The provider model is extensible, so more providers can be added.
Currently supported DNS providers:
- AWS Route 53
- Active Directory
- AXFR+DDNS
- Azure DNS
- BIND
- ClouDNS

View file

@ -7,6 +7,7 @@
<tr>
<th></th>
<th class="rotate"><div><span>ACTIVEDIRECTORY_PS</span></div></th>
<th class="rotate"><div><span>AXFRDDNS</span></div></th>
<th class="rotate"><div><span>AZURE_DNS</span></div></th>
<th class="rotate"><div><span>BIND</span></div></th>
<th class="rotate"><div><span>CLOUDFLAREAPI</span></div></th>
@ -38,6 +39,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -146,6 +150,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -206,6 +213,9 @@
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -263,6 +273,7 @@
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
@ -316,6 +327,9 @@
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can automatically manage DNSSEC">AUTODNSSEC</th>
<td><i class="fa fa-minus dim"></i></td>
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="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.">
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="Just writes out a comment indicating DNSSEC was requested">
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
@ -384,6 +398,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
@ -418,6 +435,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -473,6 +493,9 @@
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can manage NAPTR records">NAPTR</th>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -529,6 +552,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="SRV records with empty targets are not supported">
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
</td>
@ -575,6 +601,9 @@
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can manage SSHFP records">SSHFP</th>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -622,6 +651,9 @@
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can manage TLSA records">TLSA</th>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -679,6 +711,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
@ -728,6 +763,7 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
@ -749,6 +785,7 @@
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider supports Azure DNS limited ALIAS">AZURE_ALIAS</th>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -780,6 +817,9 @@
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="This driver does not manage NS records, so should not be used for dual-host scenarios">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="Azure does not permit modifying the existing NS records, only adding/removing additional records.">
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
</td>
@ -843,6 +883,9 @@
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="AD depends on the zone already existing on the dns server">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -918,6 +961,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -1014,6 +1060,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>

196
docs/_providers/axfrddns.md Normal file
View file

@ -0,0 +1,196 @@
---
name: AXFRDDNS
title: AXFR+DDNS Provider
layout: default
jsId: AXFRDDNS
---
# AXFR+DDNS Provider
This provider uses the native DNS protocols. It uses the AXFR (RFC5936,
Zone Transfer Protocol) to retrieve the existing records and DDNS
(RFC2136, Dynamic Update) to make corrections. It can use TSIG (RFC2845) or
IP-based authentication (ACLs).
It is able to work with any standards-compliant
authoritative DNS server. It has been tested with
[BIND](https://www.isc.org/bind/), [Knot](https://www.knot-dns.cz/),
and [Yadifa](https://www.yadifa.eu/home).
## Configuration
### Authentication
Authentication information is included in the `creds.json` entry for
the provider:
* `transfer-key`: If this exists, the value is used to authenticate AXFR transfers.
* `update-key`: If this exists, the value is used to authenticate DDNS updates.
For instance, your `creds.json` might looks like:
{% highlight json %}
{
"axfrddns": {
"transfer-key": "hmac-sha256:transfer-key-id:Base64EncodedSecret=",
"update-key": "hmac-sha256:update-key-id:AnotherSecret="
}
}
{% endhighlight %}
If either key is missing, DNSControl defaults to IP-based ACL
authentication for that function. Including both keys is the most
secure option. Omitting both keys defaults to IP-based ACLs for all
operations, which is the least secure option.
If distinct zones require distinct keys, you will need to instantiate the
provider once for each key:
{% highlight javascript %}
var AXFRDDNS_A = NewDnsProvider('axfrddns-a', 'AXFRDDNS'}
var AXFRDDNS_B = NewDnsProvider('axfrddns-b', 'AXFRDDNS'}
{% endhighlight %}
And update `creds.json` accordingly:
{% highlight json %}
{
"axfrddns-a": {
"transfer-key": "hmac-sha256:transfer-key-id:Base64EncodedSecret=",
"update-key": "hmac-sha256:update-key-id:AnotherSecret="
},
"axfrddns-b": {
"transfer-key": "hmac-sha512:transfer-key-id-B:SmallSecret=",
"update-key": "hmac-sha512:update-key-id-B:YetAnotherSecret="
}
}
{% endhighlight %}
### Default nameservers
The AXFR+DDNS provider can be configured with a list of default
nameservers. They will be added to all the zones handled by the
provider.
This list can be provided either as metadata or in `creds.json`. Only
the later allows `get-zones` to work properly.
{% highlight javascript %}
var AXFRDDNS = NewDnsProvider('axfrddns', 'AXFRDDNS',
'default_ns': [
'ns1.example.tld.',
'ns2.example.tld.',
'ns3.example.tld.',
'ns4.example.tld.'
]
}
{% endhighlight %}
{% highlight json %}
{
nameservers = "ns1.example.tld,ns2.example.tld,ns3.example.tld,ns4.example.tld"
}
{% endhighlight %}
### Primary master
By default, the AXFR+DDNS provider will send the AXFR requests and the
DDNS updates to the first nameserver of the zone, usually known as the
"primary master". Typically, this is the first of the default
nameservers. Though, on some networks, the primary master is a private
node, hidden behind slaves, and it does not appear in the `NS` records
of the zone. In that case, the IP or the name of the primary server
must be provided in `creds.json`. With this option, a non-standard
port might be used.
{% highlight json %}
{
master = "10.20.30.40:5353"
}
{% endhighlight %}
When no nameserver appears in the zone, and no default nameservers nor
custom master are configured, the AXFR+DDNS provider will fail with
the following error message:
{% highlight %}
[Error] AXFRDDNS: the nameservers list cannot be empty.
Please consider adding default `nameservers` or an explicit `master` in `creds.json`.
{% endhighlight %}
## Server configuration examples
### Bind9
Here is a sample `named.conf` example for an authauritative server on
zone `example.tld`. It uses a simple IP-based ACL for the AXFR
transfer and a conjunction of TSIG and IP-based ACL for the updates.
{% highlight %}
options {
listen-on { any; };
listen-on-v6 { any; };
allow-query { any; };
allow-notify { none; };
allow-recursion { none; };
allow-transfer { none; };
allow-update { none; };
allow-query-cache { none; };
};
zone "example.tld" {
type master;
file "/etc/bind/db.example.tld";
allow-transfer { example-transfer; };
allow-update { example-update; };
};
## Allow transfer to anyone on our private network
acl example-transfer {
172.17.0.0/16;
};
## Allow update only from authenticated client on our private network
acl example-update {
! {
!172.17.0.0/16;
any;
};
key update-key-id;
};
key update-key-id {
algorithm HMAC-SHA256;
secret "AnotherSecret=";
};
{% endhighlight %}
## FYI: get-zones
When using `get-zones`, a custom master or a list of default
nameservers should be configured in `creds.json`.
THe AXFR+DDNS provider does not display DNSSec records. But, if any
DNSSec records is found in the zone, it will replace all of them with
a single placeholder record:
{% highlight %}
__dnssec IN TXT "Domain has DNSSec records, not displayed here."
{% endhighlight %}
## FYI: create-domain
The AXFR+DDNS provider is not able to create domain.
## FYI: AUTODNSSEC
The AXFR+DDNS provider is not able to ask the DNS server to sign the zone. But, it is able to check whether the server seems to do so or not.
When AutoDNSSEC is set, the AXFR+DDNS provider will emit a warning when no RRSIG, DNSKEY or NSEC records are found in the zone.
When AutoDNSSEC is not set, the AXFR+DDNS provider will emit a warning when RRSIG, DNSKEY or NSEC records are found in the zone.

View file

@ -71,6 +71,7 @@ provided to help community members support their code independently.
Maintainers of contributed providers:
* `AXFRDDNS` @hnrgrgr
* `CLOUDNS` @pragmaton
* `DESEC` @D3luxee
* `DIGITALOCEAN` @Deraen

View file

@ -3,6 +3,13 @@
"ADServer": "$AD_SERVER",
"domain": "$AD_DOMAIN"
},
"AXFRDDNS": {
"master": "$AXFRDDNS_MASTER",
"nameservers": "ns.example.com",
"transfer-key": "$AXFRDDNS_TRANSFER_KEY",
"update-key": "$AXFRDDNS_UPDATE_KEY",
"domain": "$AXFRDDNS_DOMAIN"
},
"AZURE_DNS": {
"ClientID": "$AZURE_CLIENT_ID",
"ClientSecret": "$AZURE_CLIENT_SECRET",

View file

@ -4,6 +4,7 @@ package all
import (
// Define all known providers here. They should each register themselves with the providers package via init function.
_ "github.com/StackExchange/dnscontrol/v3/providers/activedir"
_ "github.com/StackExchange/dnscontrol/v3/providers/axfrddns"
_ "github.com/StackExchange/dnscontrol/v3/providers/azuredns"
_ "github.com/StackExchange/dnscontrol/v3/providers/bind"
_ "github.com/StackExchange/dnscontrol/v3/providers/cloudflare"

View file

@ -0,0 +1,383 @@
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"
"encoding/base64"
"encoding/json"
"fmt"
"math"
"math/rand"
"strings"
"time"
"github.com/miekg/dns"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/providers"
)
const (
dnsTimeout = 30 * time.Second
dnssecDummyLabel = "__dnssec"
dnssecDummyTxt = "Domain has DNSSec records, not displayed here."
)
var features = providers.DocumentationNotes{
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
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.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(),
providers.DocDualHost: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
providers.CanGetZones: providers.Can(),
}
// AxfrDdns stores the client info for the provider.
type AxfrDdns struct {
rand *rand.Rand
master 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 := &AxfrDdns{
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["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":
continue
default:
fmt.Printf("[Warning] AXFRDDNS: unknown key in `creds.json` (%s)\n", key)
}
}
return api, err
}
func init() {
providers.RegisterDomainServiceProviderType("AXFRDDNS", initAxfrDdns, 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 *AxfrDdns) GetNameservers(domain string) ([]*models.Nameserver, error) {
return c.nameservers, nil
}
// FetchZoneRecords gets the records of a zone and returns them in dns.RR format.
func (c *AxfrDdns) FetchZoneRecords(domain string) ([]dns.RR, error) {
transfer := new(dns.Transfer)
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", msg)
}
rawRecords = append(rawRecords, msg.RR...)
}
return rawRecords, nil
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (c *AxfrDdns) 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 := models.RRtoRC(rr, domain)
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 *AxfrDdns) 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)]
}
}
if dc.AutoDNSSEC && !hasDnssecRecords {
fmt.Printf("Warning: AUTODNSSEC is set, but no DNSKEY or RRSIG record was found in the AXFR answer!\n")
} else if !dc.AutoDNSSEC && hasDnssecRecords {
fmt.Printf("Warning: AUTODNSSEC is not set, but DNSKEY or RRSIG records were found in the AXFR answer!\n")
}
// Normalize
models.PostProcessRecords(foundRecords)
differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(foundRecords)
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.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
}