mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-11 18:08:57 +08:00
251 lines
7.8 KiB
Go
251 lines
7.8 KiB
Go
package desec
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"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/dnsutil"
|
|
)
|
|
|
|
/*
|
|
desec API DNS provider:
|
|
Info required in `creds.json`:
|
|
- auth-token
|
|
*/
|
|
|
|
// NewDeSec creates the provider.
|
|
func NewDeSec(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
c := &desecProvider{}
|
|
c.creds.token = m["auth-token"]
|
|
if c.creds.token == "" {
|
|
return nil, fmt.Errorf("missing deSEC auth-token")
|
|
}
|
|
if err := c.authenticate(); err != nil {
|
|
return nil, fmt.Errorf("authentication failed")
|
|
}
|
|
//DomainIndex is used for corrections (minttl) and domain creation
|
|
if err := c.initializeDomainIndex(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
var features = providers.DocumentationNotes{
|
|
providers.CanAutoDNSSEC: providers.Can("deSEC always signs all records. When trying to disable, a notice is printed."),
|
|
providers.CanGetZones: providers.Can(),
|
|
providers.CanUseAlias: providers.Unimplemented("Apex aliasing is supported via new SVCB and HTTPS record types. For details, check the deSEC docs."),
|
|
providers.CanUseCAA: providers.Can(),
|
|
providers.CanUseDS: providers.Can(),
|
|
providers.CanUseLOC: providers.Unimplemented(),
|
|
providers.CanUseNAPTR: providers.Can(),
|
|
providers.CanUsePTR: providers.Can(),
|
|
providers.CanUseSRV: providers.Can(),
|
|
providers.CanUseSSHFP: providers.Can(),
|
|
providers.CanUseTLSA: providers.Can(),
|
|
providers.DocCreateDomains: providers.Can(),
|
|
providers.DocDualHost: providers.Unimplemented(),
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
}
|
|
|
|
var defaultNameServerNames = []string{
|
|
"ns1.desec.io",
|
|
"ns2.desec.org",
|
|
}
|
|
|
|
func init() {
|
|
fns := providers.DspFuncs{
|
|
Initializer: NewDeSec,
|
|
RecordAuditor: AuditRecords,
|
|
}
|
|
providers.RegisterDomainServiceProviderType("DESEC", fns, features)
|
|
}
|
|
|
|
// GetNameservers returns the nameservers for a domain.
|
|
func (c *desecProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
return models.ToNameservers(defaultNameServerNames)
|
|
}
|
|
|
|
func (c *desecProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
if dc.AutoDNSSEC == "off" {
|
|
printer.Printf("Notice: DNSSEC signing was not requested, but cannot be turned off. (deSEC always signs all records.)\n")
|
|
}
|
|
|
|
existing, err := c.GetZoneRecords(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
models.PostProcessRecords(existing)
|
|
clean := PrepFoundRecords(existing)
|
|
var minTTL uint32
|
|
c.mutex.Lock()
|
|
if ttl, ok := c.domainIndex[dc.Name]; !ok {
|
|
minTTL = 3600
|
|
} else {
|
|
minTTL = ttl
|
|
}
|
|
c.mutex.Unlock()
|
|
PrepDesiredRecords(dc, minTTL)
|
|
return c.GenerateDomainCorrections(dc, clean)
|
|
}
|
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
func (c *desecProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|
records, err := c.getRecords(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert them to DNScontrol's native format:
|
|
existingRecords := []*models.RecordConfig{}
|
|
//spew.Dump(records)
|
|
for _, rr := range records {
|
|
existingRecords = append(existingRecords, nativeToRecords(rr, domain)...)
|
|
}
|
|
return existingRecords, nil
|
|
}
|
|
|
|
// EnsureZoneExists creates a zone if it does not exist
|
|
func (c *desecProvider) EnsureZoneExists(domain string) error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
if _, ok := c.domainIndex[domain]; ok {
|
|
return nil
|
|
}
|
|
return c.createDomain(domain)
|
|
}
|
|
|
|
// PrepFoundRecords munges any records to make them compatible with
|
|
// this provider. Usually this is a no-op.
|
|
func PrepFoundRecords(recs models.Records) models.Records {
|
|
// If there are records that need to be modified, removed, etc. we
|
|
// do it here. Usually this is a no-op.
|
|
return recs
|
|
}
|
|
|
|
// PrepDesiredRecords munges any records to best suit this provider.
|
|
func PrepDesiredRecords(dc *models.DomainConfig, minTTL uint32) {
|
|
// Sort through the dc.Records, eliminate any that can't be
|
|
// supported; modify any that need adjustments to work with the
|
|
// provider. We try to do minimal changes otherwise it gets
|
|
// confusing.
|
|
|
|
dc.Punycode()
|
|
txtutil.SplitSingleLongTxt(dc.Records)
|
|
recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
|
|
for _, rec := range dc.Records {
|
|
if rec.Type == "ALIAS" {
|
|
// deSEC does not permit ALIAS records, just ignore it
|
|
printer.Warnf("deSEC does not support alias records\n")
|
|
continue
|
|
}
|
|
if rec.TTL < minTTL {
|
|
if rec.Type != "NS" {
|
|
printer.Warnf("Please contact support@desec.io if you need TTLs < %d. Setting TTL of %s type %s from %d to %d\n", minTTL, rec.GetLabelFQDN(), rec.Type, rec.TTL, minTTL)
|
|
}
|
|
rec.TTL = minTTL
|
|
}
|
|
recordsToKeep = append(recordsToKeep, rec)
|
|
}
|
|
dc.Records = recordsToKeep
|
|
}
|
|
|
|
// GenerateDomainCorrections takes the desired and existing records
|
|
// and produces a Correction list. The correction list is simply
|
|
// a list of functions to call to actually make the desired
|
|
// correction, and a message to output to the user when the change is
|
|
// made.
|
|
func (c *desecProvider) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
|
|
|
|
var corrections []*models.Correction
|
|
var err error
|
|
var keysToUpdate map[models.RecordKey][]string
|
|
if !diff2.EnableDiff2 {
|
|
// diff existing vs. current.
|
|
keysToUpdate, err = (diff.New(dc)).ChangedGroups(existing)
|
|
} else {
|
|
keysToUpdate, err = (diff.NewCompat(dc)).ChangedGroups(existing)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(keysToUpdate) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
desiredRecords := dc.Records.GroupedByKey()
|
|
var rrs []resourceRecord
|
|
buf := &bytes.Buffer{}
|
|
// For any key with an update, delete or replace those records.
|
|
for label := range keysToUpdate {
|
|
if _, ok := desiredRecords[label]; !ok {
|
|
//we could not find this RecordKey in the desiredRecords
|
|
//this means it must be deleted
|
|
for i, msg := range keysToUpdate[label] {
|
|
if i == 0 {
|
|
rc := resourceRecord{}
|
|
rc.Type = label.Type
|
|
rc.Records = make([]string, 0) // empty array of records should delete this rrset
|
|
rc.TTL = 3600
|
|
shortname := dnsutil.TrimDomainName(label.NameFQDN, dc.Name)
|
|
if shortname == "@" {
|
|
shortname = ""
|
|
}
|
|
rc.Subname = shortname
|
|
fmt.Fprintln(buf, msg)
|
|
rrs = append(rrs, rc)
|
|
} else {
|
|
//just add the message
|
|
fmt.Fprintln(buf, msg)
|
|
}
|
|
}
|
|
} else {
|
|
//it must be an update or create, both can be done with the same api call.
|
|
ns := recordsToNative(desiredRecords[label], dc.Name)
|
|
if len(ns) > 1 {
|
|
panic("we got more than one resource record to create / modify")
|
|
}
|
|
for i, msg := range keysToUpdate[label] {
|
|
if i == 0 {
|
|
rrs = append(rrs, ns[0])
|
|
fmt.Fprintln(buf, msg)
|
|
} else {
|
|
//noop just for printing the additional messages
|
|
fmt.Fprintln(buf, msg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
msg := fmt.Sprintf("Changes:\n%s", buf)
|
|
corrections = append(corrections,
|
|
&models.Correction{
|
|
Msg: msg,
|
|
F: func() error {
|
|
rc := rrs
|
|
err := c.upsertRR(rc, dc.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
|
|
// NB(tlim): This sort is just to make updates look pretty. It is
|
|
// cosmetic. The risk here is that there may be some updates that
|
|
// require a specific order (for example a delete before an add).
|
|
// However the code doesn't seem to have such situation. All tests
|
|
// pass. That said, if this breaks anything, the easiest fix might
|
|
// be to just remove the sort.
|
|
sort.Slice(corrections, func(i, j int) bool { return diff.CorrectionLess(corrections, i, j) })
|
|
|
|
return corrections, nil
|
|
}
|