1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Fix handling of SRV records with no target (indicated by ".")

According to the RFC, the way to indicate that a SRV has no target is to set the target to ".".  Some providers do not handle this, or the API returns "" instead of ".".  This situation is now tested in the integration tests and all providers (that support this) have been fixed.



* Cloudflare: Fix decoding empty SRV target (fixes #561)

SRV records with empty (".") targets are now returned as false by
the API, which breaks Unmarshaling it into a string.

* Use custom type for Cloudflare SRV target

Rewrote the SRV target decoding to use a custom type for (un)marshaling, as
Cloudflare returns false for null targets, but it requires a single period
for giving it one. The target code has also been made more flexible to future
API changes with additional normalization.

This has been tested with record creation, deletion, and update and works
as of 2019-11-05.

* DigitalOcean: Fix target FQDN for null targets

Without this, dnscontrol thinks an update is needed (.. != .) even
when the SRV target is correct.

* DNSimple: Fix parsing of null SRV target

DNSimple only returns two fields when the target is null.

* NameDotCom: Add note about not supporting null SRV targets, skip test

* DNSimple: Do not append a . unless we have all three parts

Signed-off-by: Amelia Aronsohn <squirrel@wearing.black>

* Regenerated provider matrix
This commit is contained in:
Patrick Gaskin
2019-11-14 11:25:20 -05:00
committed by Tom Limoncelli
parent d48009a621
commit 70ce16ff23
9 changed files with 90 additions and 28 deletions

View File

@ -449,8 +449,8 @@
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="The namecheap web console allows you to make SRV records, but their api does not let you read or set them">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
<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>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -621,8 +621,8 @@
<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="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
<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>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>

View File

@ -459,6 +459,11 @@ func makeTests(t *testing.T) []*TestCase {
tc("Change Weight", srv("_sip._tcp", 52, 62, 7, "foo.com."), srv("_sip._tcp", 15, 65, 75, "foo4.com.")),
tc("Change Port", srv("_sip._tcp", 52, 62, 72, "foo.com."), srv("_sip._tcp", 15, 65, 75, "foo4.com.")),
)
if *providerToRun == "NAMEDOTCOM" {
t.Log("Skipping SRV Null Target test because provider does not support them")
} else {
tests = append(tests, tc("Null Target", srv("_sip._tcp", 52, 62, 72, "foo.com."), srv("_sip._tcp", 15, 65, 75, ".")))
}
}
// SSHFP

View File

@ -48,10 +48,14 @@ func (rc *RecordConfig) SetTargetSRVStrings(priority, weight, port, target strin
// field as the SRV priority.
func (rc *RecordConfig) SetTargetSRVPriorityString(priority uint16, s string) error {
part := strings.Fields(s)
if len(part) != 3 {
switch len(part) {
case 3:
return rc.setTargetSRVIntAndStrings(priority, part[0], part[1], part[2])
case 2:
return rc.setTargetSRVIntAndStrings(priority, part[0], part[1], ".")
default:
return errors.Errorf("SRV value does not contain 3 fields: (%#v)", s)
}
return rc.setTargetSRVIntAndStrings(priority, part[0], part[1], part[2])
}
// SetTargetSRVString is like SetTargetSRV but accepts one big string to be parsed.

View File

@ -422,23 +422,66 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
// Used on the "existing" records.
type cfRecData struct {
Name string `json:"name"`
Target string `json:"target"`
Service string `json:"service"` // SRV
Proto string `json:"proto"` // SRV
Priority uint16 `json:"priority"` // SRV
Weight uint16 `json:"weight"` // SRV
Port uint16 `json:"port"` // SRV
Tag string `json:"tag"` // CAA
Flags uint8 `json:"flags"` // CAA
Value string `json:"value"` // CAA
Usage uint8 `json:"usage"` // TLSA
Selector uint8 `json:"selector"` // TLSA
Matching_Type uint8 `json:"matching_type"` // TLSA
Certificate string `json:"certificate"` // TLSA
Algorithm uint8 `json:"algorithm"` // SSHFP
Hash_Type uint8 `json:"type"` // SSHFP
Fingerprint string `json:"fingerprint"` // SSHFP
Name string `json:"name"`
Target cfTarget `json:"target"`
Service string `json:"service"` // SRV
Proto string `json:"proto"` // SRV
Priority uint16 `json:"priority"` // SRV
Weight uint16 `json:"weight"` // SRV
Port uint16 `json:"port"` // SRV
Tag string `json:"tag"` // CAA
Flags uint8 `json:"flags"` // CAA
Value string `json:"value"` // CAA
Usage uint8 `json:"usage"` // TLSA
Selector uint8 `json:"selector"` // TLSA
Matching_Type uint8 `json:"matching_type"` // TLSA
Certificate string `json:"certificate"` // TLSA
Algorithm uint8 `json:"algorithm"` // SSHFP
Hash_Type uint8 `json:"type"` // SSHFP
Fingerprint string `json:"fingerprint"` // SSHFP
}
// cfTarget is a SRV target. A null target is represented by an empty string, but
// a dot is so acceptable.
type cfTarget string
// UnmarshalJSON decodes a SRV target from the Cloudflare API. A null target is
// represented by a false boolean or a dot. Domain names are FQDNs without a
// trailing period (as of 2019-11-05).
func (c *cfTarget) UnmarshalJSON(data []byte) error {
var obj interface{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
switch v := obj.(type) {
case string:
*c = cfTarget(v)
case bool:
if v {
panic("unknown value for cfTarget bool: true")
}
*c = "" // the "." is already added by nativeToRecord
}
return nil
}
// MarshalJSON encodes cfTarget for the Cloudflare API. Null targets are
// represented by a single period.
func (c cfTarget) MarshalJSON() ([]byte, error) {
var obj string
switch c {
case "", ".":
obj = "."
default:
obj = string(c)
}
return json.Marshal(obj)
}
// DNSControlString returns cfTarget normalized to be a FQDN. Null targets are
// represented by a single period.
func (c cfTarget) FQDN() string {
return strings.TrimRight(string(c), ".") + "."
}
type cfRecord struct {
@ -493,7 +536,7 @@ func (c *cfRecord) nativeToRecord(domain string) *models.RecordConfig {
case "SRV":
data := *c.Data
if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port,
dnsutil.AddOrigin(data.Target+".", domain)); err != nil {
dnsutil.AddOrigin(data.Target.FQDN(), domain)); err != nil {
panic(errors.Wrap(err, "unparsable SRV record received from cloudflare"))
}
default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"

View File

@ -131,15 +131,16 @@ func (c *CloudflareApi) createZone(domainName string) (string, error) {
func cfSrvData(rec *models.RecordConfig) *cfRecData {
serverParts := strings.Split(rec.GetLabelFQDN(), ".")
return &cfRecData{
c := &cfRecData{
Service: serverParts[0],
Proto: serverParts[1],
Name: strings.Join(serverParts[2:], "."),
Port: rec.SrvPort,
Priority: rec.SrvPriority,
Weight: rec.SrvWeight,
Target: rec.GetTargetField(),
}
c.Target = cfTarget(rec.GetTargetField())
return c
}
func cfCaaData(rec *models.RecordConfig) *cfRecData {

View File

@ -199,6 +199,8 @@ func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig {
// DO returns "@" on read even if fqdn was written.
if target == "@" {
target = dc.Name
} else if target == "." {
target = "" // don't append another dot to null records
}
target = dnsutil.AddOrigin(target+".", dc.Name)
// FIXME(tlim): The AddOrigin should be a no-op.

View File

@ -71,7 +71,7 @@ func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
if r.Name == "" {
r.Name = "@"
}
if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" || r.Type == "SRV" {
if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" {
r.Content += "."
}
// dnsimple adds these odd txt records that mirror the alias records.
@ -93,6 +93,10 @@ func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
panic(errors.Wrap(err, "unparsable record received from dnsimple"))
}
case "SRV":
parts := strings.Fields(r.Content)
if len(parts) == 3 {
r.Content += "."
}
if err := rec.SetTargetSRVPriorityString(uint16(r.Priority), r.Content); err != nil {
panic(errors.Wrap(err, "unparsable record received from dnsimple"))
}

View File

@ -22,7 +22,7 @@ type NameCom struct {
var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Can(),
providers.CanUsePTR: providers.Cannot("PTR records are not supported (See Link)", "https://www.name.com/support/articles/205188508-Reverse-DNS-records"),
providers.CanUseSRV: providers.Can(),
providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"),
providers.CanUseTXTMulti: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("New domains require registration"),
providers.DocDualHost: providers.Cannot("Apex NS records not editable"),

View File

@ -157,6 +157,9 @@ func (n *NameCom) createRecord(rc *models.RecordConfig, domain string) error {
case "TXT":
record.Answer = encodeTxt(rc.TxtStrings)
case "SRV":
if rc.GetTargetField() == "." {
return errors.New("SRV records with empty targets are not supported (as of 2019-11-05, the API returns 'Parameter Value Error - Invalid Srv Format')")
}
record.Answer = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
record.Priority = uint32(rc.SrvPriority)
default: