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:
@ -33,6 +33,7 @@ func generateFeatureMatrix() error {
|
|||||||
{"CAA", "Provider can manage CAA records"},
|
{"CAA", "Provider can manage CAA records"},
|
||||||
{"PTR", "Provider supports adding PTR records for reverse lookup zones"},
|
{"PTR", "Provider supports adding PTR records for reverse lookup zones"},
|
||||||
{"SRV", "Driver has explicitly implemented SRV record management"},
|
{"SRV", "Driver has explicitly implemented SRV record management"},
|
||||||
|
{"SSHFP", "Provider can manage SSHFP records"},
|
||||||
{"TLSA", "Provider can manage TLSA records"},
|
{"TLSA", "Provider can manage TLSA records"},
|
||||||
{"TXTMulti", "Provider can manage TXT records with multiple strings"},
|
{"TXTMulti", "Provider can manage TXT records with multiple strings"},
|
||||||
{"R53_ALIAS", "Provider supports Route 53 limited ALIAS"},
|
{"R53_ALIAS", "Provider supports Route 53 limited ALIAS"},
|
||||||
@ -74,6 +75,7 @@ func generateFeatureMatrix() error {
|
|||||||
setCap("CAA", providers.CanUseCAA)
|
setCap("CAA", providers.CanUseCAA)
|
||||||
setCap("PTR", providers.CanUsePTR)
|
setCap("PTR", providers.CanUsePTR)
|
||||||
setCap("SRV", providers.CanUseSRV)
|
setCap("SRV", providers.CanUseSRV)
|
||||||
|
setCap("SSHFP", providers.CanUseSSHFP)
|
||||||
setCap("TLSA", providers.CanUseTLSA)
|
setCap("TLSA", providers.CanUseTLSA)
|
||||||
setCap("TXTMulti", providers.CanUseTXTMulti)
|
setCap("TXTMulti", providers.CanUseTXTMulti)
|
||||||
setCap("R53_ALIAS", providers.CanUseRoute53Alias)
|
setCap("R53_ALIAS", providers.CanUseRoute53Alias)
|
||||||
|
@ -262,6 +262,13 @@ func srv(name string, priority, weight, port uint16, target string) *rec {
|
|||||||
return r
|
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 {
|
func txt(name, target string) *rec {
|
||||||
// FYI: This must match the algorithm in pkg/js/helpers.js TXT.
|
// FYI: This must match the algorithm in pkg/js/helpers.js TXT.
|
||||||
r := makeRec(name, target, "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
|
// CAA
|
||||||
if !providers.ProviderHasCabability(*providerToRun, providers.CanUseCAA) {
|
if !providers.ProviderHasCabability(*providerToRun, providers.CanUseCAA) {
|
||||||
t.Log("Skipping CAA Tests because provider does not support them")
|
t.Log("Skipping CAA Tests because provider does not support them")
|
||||||
|
@ -87,7 +87,7 @@ func (dc *DomainConfig) Punycode() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "A", "AAAA", "CAA", "TXT", "TLSA":
|
case "A", "AAAA", "CAA", "SSHFP", "TXT", "TLSA":
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
default:
|
default:
|
||||||
msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type)
|
msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type)
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
// NS
|
// NS
|
||||||
// PTR
|
// PTR
|
||||||
// SRV
|
// SRV
|
||||||
|
// SSHFP
|
||||||
// TLSA
|
// TLSA
|
||||||
// TXT
|
// TXT
|
||||||
// Pseudo-Types:
|
// Pseudo-Types:
|
||||||
@ -74,6 +75,8 @@ type RecordConfig struct {
|
|||||||
SrvPort uint16 `json:"srvport,omitempty"`
|
SrvPort uint16 `json:"srvport,omitempty"`
|
||||||
CaaTag string `json:"caatag,omitempty"`
|
CaaTag string `json:"caatag,omitempty"`
|
||||||
CaaFlag uint8 `json:"caaflag,omitempty"`
|
CaaFlag uint8 `json:"caaflag,omitempty"`
|
||||||
|
SshfpAlgorithm uint8 `json:"sshfpalgorithm,omitempty"`
|
||||||
|
SshfpFingerprint uint8 `json:"sshfpfingerprint,omitempty"`
|
||||||
TlsaUsage uint8 `json:"tlsausage,omitempty"`
|
TlsaUsage uint8 `json:"tlsausage,omitempty"`
|
||||||
TlsaSelector uint8 `json:"tlsaselector,omitempty"`
|
TlsaSelector uint8 `json:"tlsaselector,omitempty"`
|
||||||
TlsaMatchingType uint8 `json:"tlsamatchingtype,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).Weight = rc.SrvWeight
|
||||||
rr.(*dns.SRV).Port = rc.SrvPort
|
rr.(*dns.SRV).Port = rc.SrvPort
|
||||||
rr.(*dns.SRV).Target = rc.GetTargetField()
|
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:
|
case dns.TypeCAA:
|
||||||
rr.(*dns.CAA).Flag = rc.CaaFlag
|
rr.(*dns.CAA).Flag = rc.CaaFlag
|
||||||
rr.(*dns.CAA).Tag = rc.CaaTag
|
rr.(*dns.CAA).Tag = rc.CaaTag
|
||||||
@ -296,7 +303,7 @@ func downcase(recs []*RecordConfig) {
|
|||||||
case "ANAME", "CNAME", "MX", "NS", "PTR", "SRV":
|
case "ANAME", "CNAME", "MX", "NS", "PTR", "SRV":
|
||||||
// These record types have a target that is case insensitive, so we downcase it.
|
// These record types have a target that is case insensitive, so we downcase it.
|
||||||
r.Target = strings.ToLower(r.Target)
|
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.
|
// These record types have a target that is case sensitive, or is an IP address. We leave them alone.
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
default:
|
default:
|
||||||
|
@ -42,6 +42,8 @@ func (r *RecordConfig) PopulateFromString(rtype, contents, origin string) error
|
|||||||
return r.SetTargetMXString(contents)
|
return r.SetTargetMXString(contents)
|
||||||
case "SRV":
|
case "SRV":
|
||||||
return r.SetTargetSRVString(contents)
|
return r.SetTargetSRVString(contents)
|
||||||
|
case "SSHFP":
|
||||||
|
return r.SetTargetSSHFPString(contents)
|
||||||
case "TLSA":
|
case "TLSA":
|
||||||
return r.SetTargetTLSAString(contents)
|
return r.SetTargetTLSAString(contents)
|
||||||
case "TXT":
|
case "TXT":
|
||||||
|
52
models/t_sshfp.go
Normal file
52
models/t_sshfp.go
Normal 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]))
|
||||||
|
}
|
@ -83,6 +83,8 @@ func (rc *RecordConfig) GetTargetDebug() string {
|
|||||||
content = fmt.Sprintf("%s %s %s %d", rc.Type, rc.Name, rc.Target, rc.TTL)
|
content = fmt.Sprintf("%s %s %s %d", rc.Type, rc.Name, rc.Target, rc.TTL)
|
||||||
case "SRV":
|
case "SRV":
|
||||||
content += fmt.Sprintf(" srvpriority=%d srvweight=%d srvport=%d", rc.SrvPriority, rc.SrvWeight, rc.SrvPort)
|
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":
|
case "TLSA":
|
||||||
content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType)
|
content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType)
|
||||||
case "CAA":
|
case "CAA":
|
||||||
|
@ -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
|
// name, usage, selector, matchingtype, certificate
|
||||||
var TLSA = recordBuilder('TLSA', {
|
var TLSA = recordBuilder('TLSA', {
|
||||||
args: [
|
args: [
|
||||||
|
10
pkg/js/parse_tests/022-sshfp.js
Normal file
10
pkg/js/parse_tests/022-sshfp.js
Normal 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")
|
||||||
|
);
|
61
pkg/js/parse_tests/022-sshfp.json
Normal file
61
pkg/js/parse_tests/022-sshfp.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -58,6 +58,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
|
|||||||
"IMPORT_TRANSFORM": false,
|
"IMPORT_TRANSFORM": false,
|
||||||
"MX": true,
|
"MX": true,
|
||||||
"SRV": true,
|
"SRV": true,
|
||||||
|
"SSHFP": true,
|
||||||
"TXT": true,
|
"TXT": true,
|
||||||
"NS": true,
|
"NS": true,
|
||||||
"PTR": true,
|
"PTR": true,
|
||||||
@ -160,7 +161,7 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
|
|||||||
check(checkTarget(target))
|
check(checkTarget(target))
|
||||||
case "SRV":
|
case "SRV":
|
||||||
check(checkTarget(target))
|
check(checkTarget(target))
|
||||||
case "TXT", "IMPORT_TRANSFORM", "CAA", "TLSA":
|
case "TXT", "IMPORT_TRANSFORM", "CAA", "SSHFP", "TLSA":
|
||||||
default:
|
default:
|
||||||
if rec.Metadata["orig_custom_type"] != "" {
|
if rec.Metadata["orig_custom_type"] != "" {
|
||||||
// it is a valid custom type. We perform no validation on target
|
// it is a valid custom type. We perform no validation on target
|
||||||
|
@ -34,6 +34,7 @@ var features = providers.DocumentationNotes{
|
|||||||
providers.CanUseCAA: providers.Can(),
|
providers.CanUseCAA: providers.Can(),
|
||||||
providers.CanUsePTR: providers.Can(),
|
providers.CanUsePTR: providers.Can(),
|
||||||
providers.CanUseSRV: providers.Can(),
|
providers.CanUseSRV: providers.Can(),
|
||||||
|
providers.CanUseSSHFP: providers.Can(),
|
||||||
providers.CanUseTLSA: providers.Can(),
|
providers.CanUseTLSA: providers.Can(),
|
||||||
providers.CanUseTXTMulti: providers.Can(),
|
providers.CanUseTXTMulti: providers.Can(),
|
||||||
providers.CantUseNOPURGE: providers.Cannot(),
|
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.
|
// FIXME(tlim): SOA should be handled by splitting out the fields.
|
||||||
case *dns.SRV:
|
case *dns.SRV:
|
||||||
panicInvalid(rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target))
|
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:
|
case *dns.TLSA:
|
||||||
panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate))
|
panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate))
|
||||||
case *dns.TXT:
|
case *dns.TXT:
|
||||||
|
@ -22,6 +22,9 @@ const (
|
|||||||
// CanUseSRV indicates the provider can handle SRV records
|
// CanUseSRV indicates the provider can handle SRV records
|
||||||
CanUseSRV
|
CanUseSRV
|
||||||
|
|
||||||
|
// CanUseSSHFP indicates the provider can handle SSHFP records
|
||||||
|
CanUseSSHFP
|
||||||
|
|
||||||
// CanUseTLSA indicates the provider can handle TLSA records
|
// CanUseTLSA indicates the provider can handle TLSA records
|
||||||
CanUseTLSA
|
CanUseTLSA
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user