initial commit
This commit is contained in:
@@ -0,0 +1,541 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Field describes one input in a type-specific record editor. The web UI builds
|
||||
// its forms from this metadata, so adding a record type here gives it a proper
|
||||
// editor without writing a new template.
|
||||
type Field struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"` // text, number, textarea, select
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Help string `json:"help,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
Quote bool `json:"quote,omitempty"` // rdata field must be a quoted string
|
||||
Options []string `json:"options,omitempty"`
|
||||
Width int `json:"width,omitempty"` // Bootstrap column width, 12-grid
|
||||
}
|
||||
|
||||
// TypeInfo describes a record type and its editor.
|
||||
type TypeInfo struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Fields []Field `json:"fields"`
|
||||
Common bool `json:"common"`
|
||||
}
|
||||
|
||||
func num(key, label, placeholder, help string, width int) Field {
|
||||
return Field{Key: key, Label: label, Type: "number", Placeholder: placeholder,
|
||||
Help: help, Required: true, Width: width}
|
||||
}
|
||||
|
||||
func txt(key, label, placeholder, help string, width int) Field {
|
||||
return Field{Key: key, Label: label, Type: "text", Placeholder: placeholder,
|
||||
Help: help, Required: true, Width: width}
|
||||
}
|
||||
|
||||
// recordTypes drives both validation and the UI editors.
|
||||
var recordTypes = []TypeInfo{
|
||||
{Type: "A", Description: "IPv4 address", Common: true, Fields: []Field{
|
||||
txt("address", "IPv4 address", "192.0.2.10", "A single IPv4 address.", 12),
|
||||
}},
|
||||
{Type: "AAAA", Description: "IPv6 address", Common: true, Fields: []Field{
|
||||
txt("address", "IPv6 address", "2001:db8::10", "A single IPv6 address.", 12),
|
||||
}},
|
||||
{Type: "CNAME", Description: "Canonical name alias", Common: true, Fields: []Field{
|
||||
txt("target", "Target", "example.com.", "The name this record is an alias for. End with a dot for an absolute name.", 12),
|
||||
}},
|
||||
{Type: "MX", Description: "Mail exchanger", Common: true, Fields: []Field{
|
||||
num("preference", "Preference", "10", "Lower values are preferred.", 3),
|
||||
txt("exchange", "Mail server", "mail.example.com.", "Host name of the mail server. Must not be a CNAME.", 9),
|
||||
}},
|
||||
{Type: "TXT", Description: "Free-form text", Common: true, Fields: []Field{
|
||||
{Key: "text", Label: "Text", Type: "textarea", Required: true, Quote: true, Width: 12,
|
||||
Placeholder: "v=spf1 include:_spf.example.com ~all",
|
||||
Help: "Quoting and 255-character chunking are handled automatically."},
|
||||
}},
|
||||
{Type: "NS", Description: "Name server delegation", Common: true, Fields: []Field{
|
||||
txt("nameserver", "Name server", "ns1.example.com.", "Authoritative name server for this name.", 12),
|
||||
}},
|
||||
{Type: "SRV", Description: "Service location", Common: true, Fields: []Field{
|
||||
num("priority", "Priority", "10", "Lower values are preferred.", 3),
|
||||
num("weight", "Weight", "20", "Relative weight among equal priorities.", 3),
|
||||
num("port", "Port", "5060", "TCP or UDP port of the service.", 3),
|
||||
txt("target", "Target", "sip.example.com.", "Host providing the service.", 3),
|
||||
}},
|
||||
{Type: "PTR", Description: "Reverse pointer", Common: true, Fields: []Field{
|
||||
txt("target", "Points to", "host.example.com.", "The host name this address belongs to.", 12),
|
||||
}},
|
||||
{Type: "CAA", Description: "Certificate authority authorisation", Common: true, Fields: []Field{
|
||||
num("flags", "Flags", "0", "128 marks the property as critical.", 2),
|
||||
{Key: "tag", Label: "Tag", Type: "select", Required: true, Width: 3,
|
||||
Options: []string{"issue", "issuewild", "iodef", "contactemail", "contactphone"}},
|
||||
{Key: "value", Label: "Value", Type: "text", Required: true, Quote: true, Width: 7,
|
||||
Placeholder: "letsencrypt.org", Help: "The CA domain, or a mailto:/https: URL for iodef."},
|
||||
}},
|
||||
{Type: "SOA", Description: "Start of authority", Fields: []Field{
|
||||
txt("ns", "Primary name server", "ns1.example.com.", "", 6),
|
||||
txt("mbox", "Responsible party", "hostmaster.example.com.", "The @ in the email address becomes a dot.", 6),
|
||||
num("serial", "Serial", "1", "", 4),
|
||||
num("refresh", "Refresh", "7200", "", 4),
|
||||
num("retry", "Retry", "3600", "", 4),
|
||||
num("expire", "Expire", "1209600", "", 6),
|
||||
num("minimum", "Minimum / negative TTL", "3600", "", 6),
|
||||
}},
|
||||
{Type: "NAPTR", Description: "Naming authority pointer", Fields: []Field{
|
||||
num("order", "Order", "100", "", 3),
|
||||
num("preference", "Preference", "10", "", 3),
|
||||
{Key: "flags", Label: "Flags", Type: "text", Quote: true, Width: 3, Placeholder: "U"},
|
||||
{Key: "service", Label: "Service", Type: "text", Quote: true, Width: 3, Placeholder: "E2U+sip"},
|
||||
{Key: "regexp", Label: "Regexp", Type: "text", Quote: true, Width: 8,
|
||||
Placeholder: `!^.*$!sip:info@example.com!`},
|
||||
txt("replacement", "Replacement", ".", "Use a single dot when a regexp is given.", 4),
|
||||
}},
|
||||
{Type: "TLSA", Description: "TLS certificate association", Fields: []Field{
|
||||
num("usage", "Usage", "3", "0-3; 3 is a domain-issued certificate.", 3),
|
||||
num("selector", "Selector", "1", "0 full certificate, 1 public key.", 3),
|
||||
num("matching_type", "Matching type", "1", "0 exact, 1 SHA-256, 2 SHA-512.", 3),
|
||||
txt("certificate", "Certificate data", "abc123...", "Hex encoded association data.", 3),
|
||||
}},
|
||||
{Type: "SSHFP", Description: "SSH host key fingerprint", Fields: []Field{
|
||||
num("algorithm", "Algorithm", "4", "1 RSA, 2 DSA, 3 ECDSA, 4 Ed25519.", 4),
|
||||
num("type", "Fingerprint type", "2", "1 SHA-1, 2 SHA-256.", 4),
|
||||
txt("fingerprint", "Fingerprint", "abc123...", "Hex encoded fingerprint.", 4),
|
||||
}},
|
||||
{Type: "SVCB", Description: "Service binding", Fields: []Field{
|
||||
num("priority", "Priority", "1", "0 selects alias mode.", 3),
|
||||
txt("target", "Target", "svc.example.com.", "", 4),
|
||||
{Key: "params", Label: "Parameters", Type: "text", Width: 5,
|
||||
Placeholder: "alpn=h2,h3 port=8443", Help: "Space separated key=value pairs."},
|
||||
}},
|
||||
{Type: "HTTPS", Description: "HTTPS service binding", Common: true, Fields: []Field{
|
||||
num("priority", "Priority", "1", "0 selects alias mode.", 3),
|
||||
txt("target", "Target", ".", "A single dot means the owner name itself.", 4),
|
||||
{Key: "params", Label: "Parameters", Type: "text", Width: 5,
|
||||
Placeholder: "alpn=h2,h3 ipv4hint=192.0.2.10", Help: "Space separated key=value pairs."},
|
||||
}},
|
||||
{Type: "DS", Description: "Delegation signer", Fields: []Field{
|
||||
num("key_tag", "Key tag", "12345", "", 3),
|
||||
num("algorithm", "Algorithm", "13", "8 RSASHA256, 13 ECDSAP256SHA256, 15 ED25519.", 3),
|
||||
num("digest_type", "Digest type", "2", "1 SHA-1, 2 SHA-256, 4 SHA-384.", 3),
|
||||
txt("digest", "Digest", "abc123...", "Hex encoded digest.", 3),
|
||||
}},
|
||||
{Type: "DNSKEY", Description: "DNSSEC public key", Fields: []Field{
|
||||
num("flags", "Flags", "257", "256 zone signing key, 257 key signing key.", 3),
|
||||
num("protocol", "Protocol", "3", "Always 3.", 3),
|
||||
num("algorithm", "Algorithm", "13", "8 RSASHA256, 13 ECDSAP256SHA256, 15 ED25519.", 3),
|
||||
{Key: "public_key", Label: "Public key", Type: "textarea", Required: true, Width: 12,
|
||||
Placeholder: "base64 encoded key material"},
|
||||
}},
|
||||
{Type: "DNAME", Description: "Delegation name redirection", Fields: []Field{
|
||||
txt("target", "Target", "example.net.", "Rewrites the entire subtree below this name.", 12),
|
||||
}},
|
||||
{Type: "SPF", Description: "Legacy sender policy (prefer TXT)", Fields: []Field{
|
||||
{Key: "text", Label: "Policy", Type: "textarea", Required: true, Quote: true, Width: 12,
|
||||
Placeholder: "v=spf1 mx ~all"},
|
||||
}},
|
||||
{Type: "LOC", Description: "Geographic location", Fields: []Field{
|
||||
{Key: "raw", Label: "Location", Type: "text", Required: true, Width: 12,
|
||||
Placeholder: "51 30 12.748 N 0 7 39.611 W 0.00m"},
|
||||
}},
|
||||
{Type: "RAW", Description: "Advanced: any record type, entered by hand", Fields: []Field{
|
||||
{Key: "rtype", Label: "Record type", Type: "text", Required: true, Width: 4,
|
||||
Placeholder: "URI", Help: "Any type name known to the DNS library, or TYPE65534 for unknown types."},
|
||||
{Key: "rdata", Label: "Record data", Type: "textarea", Required: true, Width: 8,
|
||||
Placeholder: `10 1 "https://example.com/"`,
|
||||
Help: "Rdata exactly as it would appear in a zone file. Unknown types use the RFC 3597 form: \\# 4 0A0B0C0D"},
|
||||
}},
|
||||
}
|
||||
|
||||
// TypeInfos returns the record type catalogue used by the UI.
|
||||
func TypeInfos() []TypeInfo { return recordTypes }
|
||||
|
||||
// TypeInfoFor looks up one record type's editor definition.
|
||||
func TypeInfoFor(t string) (TypeInfo, bool) {
|
||||
t = strings.ToUpper(strings.TrimSpace(t))
|
||||
for _, ti := range recordTypes {
|
||||
if ti.Type == t {
|
||||
return ti, true
|
||||
}
|
||||
}
|
||||
return TypeInfo{}, false
|
||||
}
|
||||
|
||||
// KnownType reports whether the DNS library understands a type name. This
|
||||
// accepts far more types than have dedicated editors, including the RFC 3597
|
||||
// TYPEnnnnn form.
|
||||
func KnownType(t string) bool {
|
||||
t = strings.ToUpper(strings.TrimSpace(t))
|
||||
if _, ok := dns.StringToType[t]; ok {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(t, "TYPE") {
|
||||
if n, err := strconv.Atoi(t[4:]); err == nil && n > 0 && n <= 65535 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NormaliseType uppercases and checks a record type name.
|
||||
func NormaliseType(t string) (string, error) {
|
||||
t = strings.ToUpper(strings.TrimSpace(t))
|
||||
if t == "" {
|
||||
return "", errors.New("record type must not be empty")
|
||||
}
|
||||
if t == "RAW" {
|
||||
return "", errors.New("choose a concrete record type in the advanced editor")
|
||||
}
|
||||
if !KnownType(t) {
|
||||
return "", fmt.Errorf("%q is not a known DNS record type; "+
|
||||
"use the advanced editor with the TYPEnnnnn form for unassigned types", t)
|
||||
}
|
||||
switch t {
|
||||
case "ANY", "AXFR", "IXFR", "OPT", "TSIG", "TKEY":
|
||||
return "", fmt.Errorf("%s is a meta record type and cannot be stored in a zone", t)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// BuildRR assembles and validates a resource record.
|
||||
//
|
||||
// The zone origin is handed to the parser so that relative names in rdata (and
|
||||
// "@") resolve exactly the way they would in a real zone file.
|
||||
func BuildRR(zone, name, rtype, data string, ttl uint32) (dns.RR, error) {
|
||||
rtype, err := NormaliseType(rtype)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = strings.TrimSpace(data)
|
||||
if data == "" {
|
||||
return nil, fmt.Errorf("%s record data must not be empty", rtype)
|
||||
}
|
||||
if err := preflight(rtype, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owner := name
|
||||
if owner == "" {
|
||||
owner = "@"
|
||||
}
|
||||
line := fmt.Sprintf("%s %d IN %s %s", owner, ttl, rtype, data)
|
||||
|
||||
zp := dns.NewZoneParser(strings.NewReader(line), zone, "record")
|
||||
zp.SetDefaultTTL(ttl)
|
||||
rr, ok := zp.Next()
|
||||
if err := zp.Err(); err != nil {
|
||||
return nil, fmt.Errorf("invalid %s record data: %s", rtype, cleanParseError(err))
|
||||
}
|
||||
if !ok || rr == nil {
|
||||
return nil, fmt.Errorf("invalid %s record data: %q could not be parsed", rtype, data)
|
||||
}
|
||||
if _, more := zp.Next(); more {
|
||||
return nil, fmt.Errorf("%s record data must be a single record", rtype)
|
||||
}
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
// preflight catches the mistakes users actually make, so they get a sentence
|
||||
// they can act on instead of a parser offset.
|
||||
func preflight(rtype, data string) error {
|
||||
switch rtype {
|
||||
case "A":
|
||||
addr, err := netip.ParseAddr(strings.TrimSpace(data))
|
||||
if err != nil || !addr.Is4() {
|
||||
return fmt.Errorf("an A record needs a valid IPv4 address, for example 192.0.2.10 (got %q)", data)
|
||||
}
|
||||
case "AAAA":
|
||||
addr, err := netip.ParseAddr(strings.TrimSpace(data))
|
||||
if err != nil || addr.Is4() {
|
||||
return fmt.Errorf("an AAAA record needs a valid IPv6 address, for example 2001:db8::10 (got %q)", data)
|
||||
}
|
||||
case "MX":
|
||||
f := strings.Fields(data)
|
||||
if len(f) != 2 {
|
||||
return fmt.Errorf("an MX record needs a preference and a host name, for example: 10 mail.example.com.")
|
||||
}
|
||||
if _, err := strconv.ParseUint(f[0], 10, 16); err != nil {
|
||||
return fmt.Errorf("the MX preference %q must be a number between 0 and 65535", f[0])
|
||||
}
|
||||
case "SRV":
|
||||
f := strings.Fields(data)
|
||||
if len(f) != 4 {
|
||||
return fmt.Errorf("an SRV record needs priority, weight, port and target, " +
|
||||
"for example: 10 20 5060 sip.example.com.")
|
||||
}
|
||||
for i, label := range []string{"priority", "weight", "port"} {
|
||||
if _, err := strconv.ParseUint(f[i], 10, 16); err != nil {
|
||||
return fmt.Errorf("the SRV %s %q must be a number between 0 and 65535", label, f[i])
|
||||
}
|
||||
}
|
||||
case "CAA":
|
||||
f := strings.Fields(data)
|
||||
if len(f) < 3 {
|
||||
return fmt.Errorf(`a CAA record needs flags, a tag and a quoted value, ` +
|
||||
`for example: 0 issue "letsencrypt.org"`)
|
||||
}
|
||||
case "CNAME", "PTR", "NS", "DNAME":
|
||||
if strings.Fields(data) == nil || len(strings.Fields(data)) != 1 {
|
||||
return fmt.Errorf("a %s record takes exactly one name, for example: host.example.com.", rtype)
|
||||
}
|
||||
case "TXT", "SPF":
|
||||
if !strings.HasPrefix(strings.TrimSpace(data), `"`) {
|
||||
return fmt.Errorf("%s record data must be quoted; the record editor does this for you", rtype)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanParseError strips the parser's file/line noise, which is meaningless
|
||||
// for a single record typed into a form.
|
||||
func cleanParseError(err error) string {
|
||||
msg := err.Error()
|
||||
if i := strings.Index(msg, "dns: "); i >= 0 {
|
||||
msg = msg[i+len("dns: "):]
|
||||
}
|
||||
if i := strings.Index(msg, " at record:"); i >= 0 {
|
||||
msg = msg[:i]
|
||||
}
|
||||
if i := strings.Index(msg, "\" at line"); i >= 0 {
|
||||
msg = msg[:i+1]
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// QuoteTXT turns free text into one or more quoted character-strings, splitting
|
||||
// at the 255 byte limit that a single DNS character-string may hold.
|
||||
func QuoteTXT(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return `""`
|
||||
}
|
||||
// Already-quoted input is passed through so operators can hand-craft
|
||||
// multi-string records.
|
||||
if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) && len(s) > 1 {
|
||||
return s
|
||||
}
|
||||
const maxChunk = 255
|
||||
var chunks []string
|
||||
for len(s) > maxChunk {
|
||||
chunks = append(chunks, s[:maxChunk])
|
||||
s = s[maxChunk:]
|
||||
}
|
||||
chunks = append(chunks, s)
|
||||
for i, c := range chunks {
|
||||
chunks[i] = `"` + escapeCharString(c) + `"`
|
||||
}
|
||||
return strings.Join(chunks, " ")
|
||||
}
|
||||
|
||||
func escapeCharString(s string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(s) + 8)
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"', '\\':
|
||||
b.WriteByte('\\')
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// AssembleRData joins editor field values into zone-file rdata, applying
|
||||
// quoting where the record type requires it.
|
||||
func AssembleRData(rtype string, values map[string]string) (string, error) {
|
||||
info, ok := TypeInfoFor(rtype)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no editor is defined for record type %q", rtype)
|
||||
}
|
||||
var parts []string
|
||||
for _, f := range info.Fields {
|
||||
v := strings.TrimSpace(values[f.Key])
|
||||
if v == "" {
|
||||
if f.Required {
|
||||
return "", fmt.Errorf("%s is required for a %s record", f.Label, rtype)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if f.Quote {
|
||||
v = QuoteTXT(v)
|
||||
}
|
||||
parts = append(parts, v)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "", fmt.Errorf("%s record data must not be empty", rtype)
|
||||
}
|
||||
return strings.Join(parts, " "), nil
|
||||
}
|
||||
|
||||
// SplitRData splits stored rdata back into editor field values so an existing
|
||||
// record can be edited in its dedicated form.
|
||||
func SplitRData(rtype, data string) map[string]string {
|
||||
out := map[string]string{}
|
||||
info, ok := TypeInfoFor(rtype)
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
// Types whose final field swallows the remainder of the line.
|
||||
fields := info.Fields
|
||||
if len(fields) == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
// A single quoted field (TXT, SPF) takes the whole rdata verbatim.
|
||||
if len(fields) == 1 {
|
||||
v := data
|
||||
if fields[0].Quote {
|
||||
v = UnquoteTXT(data)
|
||||
}
|
||||
out[fields[0].Key] = v
|
||||
return out
|
||||
}
|
||||
|
||||
toks := tokeniseRData(data)
|
||||
for i, f := range fields {
|
||||
if i >= len(toks) {
|
||||
break
|
||||
}
|
||||
if i == len(fields)-1 && len(toks) > len(fields) {
|
||||
// Trailing field absorbs everything left (e.g. SVCB parameters).
|
||||
rest := strings.Join(toks[i:], " ")
|
||||
if f.Quote {
|
||||
rest = UnquoteTXT(rest)
|
||||
}
|
||||
out[f.Key] = rest
|
||||
break
|
||||
}
|
||||
v := toks[i]
|
||||
if f.Quote {
|
||||
v = UnquoteTXT(v)
|
||||
}
|
||||
out[f.Key] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// tokeniseRData splits on whitespace while keeping quoted strings together.
|
||||
func tokeniseRData(s string) []string {
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
inQuote, escaped, started := false, false, false
|
||||
flush := func() {
|
||||
if started {
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
started = false
|
||||
}
|
||||
}
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case escaped:
|
||||
cur.WriteRune(r)
|
||||
escaped = false
|
||||
started = true
|
||||
case r == '\\':
|
||||
cur.WriteRune(r)
|
||||
escaped = true
|
||||
started = true
|
||||
case r == '"':
|
||||
cur.WriteRune(r)
|
||||
inQuote = !inQuote
|
||||
started = true
|
||||
case (r == ' ' || r == '\t') && !inQuote:
|
||||
flush()
|
||||
default:
|
||||
cur.WriteRune(r)
|
||||
started = true
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
|
||||
// UnquoteTXT reverses QuoteTXT, concatenating adjacent character-strings.
|
||||
func UnquoteTXT(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if !strings.Contains(s, `"`) {
|
||||
return s
|
||||
}
|
||||
var b strings.Builder
|
||||
inQuote, escaped := false, false
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case escaped:
|
||||
b.WriteRune(r)
|
||||
escaped = false
|
||||
case r == '\\' && inQuote:
|
||||
escaped = true
|
||||
case r == '"':
|
||||
inQuote = !inQuote
|
||||
case inQuote:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// CNAMEConflict reports whether adding a record of type newType at a name that
|
||||
// already holds the given types would create an illegal combination.
|
||||
//
|
||||
// RFC 1034 forbids a CNAME from coexisting with any other data at the same
|
||||
// name, with the DNSSEC types being the standard exception.
|
||||
func CNAMEConflict(newType string, existing []string) error {
|
||||
newType = strings.ToUpper(newType)
|
||||
hasCNAME := false
|
||||
otherTypes := 0
|
||||
for _, t := range existing {
|
||||
t = strings.ToUpper(t)
|
||||
if t == "CNAME" {
|
||||
hasCNAME = true
|
||||
continue
|
||||
}
|
||||
if !dnssecCompatible(t) {
|
||||
otherTypes++
|
||||
}
|
||||
}
|
||||
if newType == "CNAME" {
|
||||
if hasCNAME {
|
||||
return errors.New("this name already has a CNAME record; a name may only have one")
|
||||
}
|
||||
if otherTypes > 0 {
|
||||
return errors.New("a CNAME cannot coexist with other records at the same name; " +
|
||||
"remove the existing records or use a different name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if hasCNAME && !dnssecCompatible(newType) {
|
||||
return fmt.Errorf("this name already has a CNAME record, so it cannot also have a %s record", newType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dnssecCompatible reports whether a type is permitted alongside a CNAME.
|
||||
func dnssecCompatible(t string) bool {
|
||||
switch strings.ToUpper(t) {
|
||||
case "RRSIG", "NSEC", "NSEC3", "KEY", "DS":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ApexRestricted reports whether a record type is illegal at a zone apex.
|
||||
func ApexRestricted(rtype string) error {
|
||||
switch strings.ToUpper(rtype) {
|
||||
case "CNAME":
|
||||
return errors.New("a CNAME cannot be placed at the zone apex; " +
|
||||
"use an A, AAAA or HTTPS record instead")
|
||||
case "DNAME":
|
||||
return errors.New("a DNAME cannot be placed at the zone apex")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user