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

Add SSHFP DNS record support. (#439)

* Add SSHFP DNS record support.
* Fix integration test.
This commit is contained in:
karlism
2019-01-28 23:26:20 +01:00
committed by Tom Limoncelli
parent f96a2189a1
commit f9fc6243d4
13 changed files with 183 additions and 3 deletions

View File

@ -33,6 +33,7 @@ func generateFeatureMatrix() error {
{"CAA", "Provider can manage CAA records"},
{"PTR", "Provider supports adding PTR records for reverse lookup zones"},
{"SRV", "Driver has explicitly implemented SRV record management"},
{"SSHFP", "Provider can manage SSHFP records"},
{"TLSA", "Provider can manage TLSA records"},
{"TXTMulti", "Provider can manage TXT records with multiple strings"},
{"R53_ALIAS", "Provider supports Route 53 limited ALIAS"},
@ -74,6 +75,7 @@ func generateFeatureMatrix() error {
setCap("CAA", providers.CanUseCAA)
setCap("PTR", providers.CanUsePTR)
setCap("SRV", providers.CanUseSRV)
setCap("SSHFP", providers.CanUseSSHFP)
setCap("TLSA", providers.CanUseTLSA)
setCap("TXTMulti", providers.CanUseTXTMulti)
setCap("R53_ALIAS", providers.CanUseRoute53Alias)

View File

@ -262,6 +262,13 @@ func srv(name string, priority, weight, port uint16, target string) *rec {
return r
}
func sshfp(name string, algorithm uint8, fingerprint uint8, target string) *rec {
r := makeRec(name, target, "SSHFP")
r.SshfpAlgorithm = algorithm
r.SshfpFingerprint = fingerprint
return r
}
func txt(name, target string) *rec {
// FYI: This must match the algorithm in pkg/js/helpers.js TXT.
r := makeRec(name, target, "TXT")
@ -426,6 +433,20 @@ func makeTests(t *testing.T) []*TestCase {
)
}
// SSHFP
if !providers.ProviderHasCabability(*providerToRun, providers.CanUseSSHFP) {
t.Log("Skipping SSHFP Tests because provider does not support them")
} else {
tests = append(tests, tc("Empty"),
tc("SSHFP record", sshfp("@", 1, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")),
tc("SSHFP change algorithm", sshfp("@", 2, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")),
tc("SSHFP change value", sshfp("@", 2, 1, "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc")),
tc("SSHFP change fingerprint", sshfp("@", 2, 2, "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc")),
tc("SSHFP many records", sshfp("@", 1, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"), sshfp("@", 1, 2, "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"), sshfp("@", 2, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")),
tc("SSHFP delete", sshfp("@", 1, 1, "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c")),
)
}
// CAA
if !providers.ProviderHasCabability(*providerToRun, providers.CanUseCAA) {
t.Log("Skipping CAA Tests because provider does not support them")

View File

@ -87,7 +87,7 @@ func (dc *DomainConfig) Punycode() error {
if err != nil {
return err
}
case "A", "AAAA", "CAA", "TXT", "TLSA":
case "A", "AAAA", "CAA", "SSHFP", "TXT", "TLSA":
// Nothing to do.
default:
msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type)

View File

@ -22,6 +22,7 @@ import (
// NS
// PTR
// SRV
// SSHFP
// TLSA
// TXT
// Pseudo-Types:
@ -74,6 +75,8 @@ type RecordConfig struct {
SrvPort uint16 `json:"srvport,omitempty"`
CaaTag string `json:"caatag,omitempty"`
CaaFlag uint8 `json:"caaflag,omitempty"`
SshfpAlgorithm uint8 `json:"sshfpalgorithm,omitempty"`
SshfpFingerprint uint8 `json:"sshfpfingerprint,omitempty"`
TlsaUsage uint8 `json:"tlsausage,omitempty"`
TlsaSelector uint8 `json:"tlsaselector,omitempty"`
TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"`
@ -218,6 +221,10 @@ func (rc *RecordConfig) ToRR() dns.RR {
rr.(*dns.SRV).Weight = rc.SrvWeight
rr.(*dns.SRV).Port = rc.SrvPort
rr.(*dns.SRV).Target = rc.GetTargetField()
case dns.TypeSSHFP:
rr.(*dns.SSHFP).Algorithm = rc.SshfpAlgorithm
rr.(*dns.SSHFP).Type = rc.SshfpFingerprint
rr.(*dns.SSHFP).FingerPrint = rc.GetTargetField()
case dns.TypeCAA:
rr.(*dns.CAA).Flag = rc.CaaFlag
rr.(*dns.CAA).Tag = rc.CaaTag
@ -296,7 +303,7 @@ func downcase(recs []*RecordConfig) {
case "ANAME", "CNAME", "MX", "NS", "PTR", "SRV":
// These record types have a target that is case insensitive, so we downcase it.
r.Target = strings.ToLower(r.Target)
case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SOA", "CF_REDIRECT", "CF_TEMP_REDIRECT":
case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SOA", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT":
// These record types have a target that is case sensitive, or is an IP address. We leave them alone.
// Do nothing.
default:

View File

@ -42,6 +42,8 @@ func (r *RecordConfig) PopulateFromString(rtype, contents, origin string) error
return r.SetTargetMXString(contents)
case "SRV":
return r.SetTargetSRVString(contents)
case "SSHFP":
return r.SetTargetSSHFPString(contents)
case "TLSA":
return r.SetTargetTLSAString(contents)
case "TXT":

52
models/t_sshfp.go Normal file
View File

@ -0,0 +1,52 @@
package models
import (
"strconv"
"strings"
"github.com/pkg/errors"
)
// SetTargetSSHFP sets the SSHFP fields.
func (rc *RecordConfig) SetTargetSSHFP(algorithm uint8, fingerprint uint8, target string) error {
rc.SshfpAlgorithm = algorithm
rc.SshfpFingerprint = fingerprint
rc.SetTarget(target)
if rc.Type == "" {
rc.Type = "SSHFP"
}
if rc.Type != "SSHFP" {
panic("assertion failed: SetTargetSSHFP called when .Type is not SSHFP")
}
if algorithm < 1 && algorithm > 4 {
return errors.Errorf("SSHFP algorithm (%v) is not one of 1, 2, 3 or 4", algorithm)
}
if fingerprint < 1 && fingerprint > 2 {
return errors.Errorf("SSHFP fingerprint (%v) is not one of 1 or 2", fingerprint)
}
return nil
}
// SetTargetSSHFPStrings is like SetTargetSSHFP but accepts strings.
func (rc *RecordConfig) SetTargetSSHFPStrings(algorithm, fingerprint, target string) error {
i64algorithm, err := strconv.ParseUint(algorithm, 10, 8)
if err != nil {
return errors.Wrap(err, "SSHFP algorithm does not fit in 8 bits")
}
i64fingerprint, err := strconv.ParseUint(fingerprint, 10, 8)
if err != nil {
return errors.Wrap(err, "SSHFP fingerprint does not fit in 8 bits")
}
return rc.SetTargetSSHFP(uint8(i64algorithm), uint8(i64fingerprint), target)
}
// SetTargetSSHFPString is like SetTargetSSHFP but accepts one big string.
func (rc *RecordConfig) SetTargetSSHFPString(s string) error {
part := strings.Fields(s)
if len(part) != 3 {
return errors.Errorf("SSHFP value does not contain 3 fields: (%#v)", s)
}
return rc.SetTargetSSHFPStrings(part[0], part[1], StripQuotes(part[2]))
}

View File

@ -83,6 +83,8 @@ func (rc *RecordConfig) GetTargetDebug() string {
content = fmt.Sprintf("%s %s %s %d", rc.Type, rc.Name, rc.Target, rc.TTL)
case "SRV":
content += fmt.Sprintf(" srvpriority=%d srvweight=%d srvport=%d", rc.SrvPriority, rc.SrvWeight, rc.SrvPort)
case "SSHFP":
content += fmt.Sprintf(" sshfpalgorithm=%d sshfpfingerprint=%d", rc.SshfpAlgorithm, rc.SshfpFingerprint)
case "TLSA":
content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType)
case "CAA":

View File

@ -235,6 +235,22 @@ var SRV = recordBuilder('SRV', {
},
});
// SSHFP(name,algorithm,type,value, recordModifiers...)
var SSHFP = recordBuilder('SSHFP', {
args: [
['name', _.isString],
['algorithm', _.isNumber],
['fingerprint', _.isNumber],
['value', _.isString],
],
transform: function(record, args, modifiers) {
record.name = args.name;
record.sshfpalgorithm = args.algorithm;
record.sshfpfingerprint = args.fingerprint;
record.target = args.value;
},
});
// name, usage, selector, matchingtype, certificate
var TLSA = recordBuilder('TLSA', {
args: [

View File

@ -0,0 +1,10 @@
D("foo.com","none",
SSHFP("@",1,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"),
SSHFP("@",1,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"),
SSHFP("@",2,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"),
SSHFP("@",2,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"),
SSHFP("@",3,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"),
SSHFP("@",3,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"),
SSHFP("@",4,1,"66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"),
SSHFP("@",4,2,"745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc")
);

View File

@ -0,0 +1,61 @@
{
"registrars": [],
"dns_providers": [],
"domains": [
{
"name": "foo.com",
"registrar": "none",
"dnsProviders": {},
"records": [
{
"type": "SSHFP",
"algorithm": 1,
"fingerprint": 1,
"value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"
},
{
"type": "SSHFP",
"algorithm": 1,
"fingerprint": 2,
"value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"
},
{
"type": "SSHFP",
"algorithm": 2,
"fingerprint": 1,
"value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"
},
{
"type": "SSHFP",
"algorithm": 2,
"fingerprint": 2,
"value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"
},
{
"type": "SSHFP",
"algorithm": 3,
"fingerprint": 1,
"value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"
},
{
"type": "SSHFP",
"algorithm": 3,
"fingerprint": 2,
"value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"
},
{
"type": "SSHFP",
"algorithm": 4,
"fingerprint": 1,
"value": "66c7d5540b7d75a1fb4c84febfa178ad99bdd67c"
},
{
"type": "SSHFP",
"algorithm": 4,
"fingerprint": 2,
"value": "745a635bc46a397a5c4f21d437483005bcc40d7511ff15fbfafe913a081559bc"
}
]
}
]
}

View File

@ -58,6 +58,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
"IMPORT_TRANSFORM": false,
"MX": true,
"SRV": true,
"SSHFP": true,
"TXT": true,
"NS": true,
"PTR": true,
@ -160,7 +161,7 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
check(checkTarget(target))
case "SRV":
check(checkTarget(target))
case "TXT", "IMPORT_TRANSFORM", "CAA", "TLSA":
case "TXT", "IMPORT_TRANSFORM", "CAA", "SSHFP", "TLSA":
default:
if rec.Metadata["orig_custom_type"] != "" {
// it is a valid custom type. We perform no validation on target

View File

@ -34,6 +34,7 @@ var features = providers.DocumentationNotes{
providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CantUseNOPURGE: providers.Cannot(),
@ -137,6 +138,8 @@ func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordCo
// FIXME(tlim): SOA should be handled by splitting out the fields.
case *dns.SRV:
panicInvalid(rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target))
case *dns.SSHFP:
panicInvalid(rc.SetTargetSSHFP(v.Algorithm, v.Type, v.FingerPrint))
case *dns.TLSA:
panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate))
case *dns.TXT:

View File

@ -22,6 +22,9 @@ const (
// CanUseSRV indicates the provider can handle SRV records
CanUseSRV
// CanUseSSHFP indicates the provider can handle SSHFP records
CanUseSSHFP
// CanUseTLSA indicates the provider can handle TLSA records
CanUseTLSA