mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-10 17:38:13 +08:00
* 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:
parent
ec27d2c76a
commit
8dd66ec605
8 changed files with 639 additions and 0 deletions
1
OWNERS
1
OWNERS
|
@ -1,4 +1,5 @@
|
|||
# providers/activedir
|
||||
providers/axfrddns @hnrgrgr
|
||||
providers/azuredns @vatsalyagoel
|
||||
providers/bind @tlimoncelli
|
||||
# providers/cloudflare
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
196
docs/_providers/axfrddns.md
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
383
providers/axfrddns/axfrddnsProvider.go
Normal file
383
providers/axfrddns/axfrddnsProvider.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue