Source code for octodns.zone.cname
#
#
#
from .base import Zone
from .validator import ValidationReason, ZoneValidator
[docs]
class CnameCoexistenceValidator(ZoneValidator):
'''
Verify that CNAME records do not coexist with other records at the same
node, and ALIAS records do not coexist with A or AAAA records.
'''
[docs]
def validate(self, zone):
reasons = []
for node in zone._records.values():
if len(node) > 1:
types = [r._type for r in node]
if 'CNAME' in types:
# All records at this node have the same FQDN
record = next(r for r in node if r._type == 'CNAME')
reasons.append(
ValidationReason(
f'Invalid state, CNAME at {record.fqdn} cannot coexist with other records',
node,
)
)
elif 'ALIAS' in types and ('A' in types or 'AAAA' in types):
record = next(r for r in node if r._type == 'ALIAS')
reasons.append(
ValidationReason(
f'Invalid state, ALIAS at {record.fqdn} cannot coexist with A or AAAA records',
node,
)
)
return reasons
[docs]
class NoCnameLoopZoneValidator(ZoneValidator):
'''
Checks for circular CNAME or ALIAS chains within the zone. Circular
references prevent DNS resolution from ever completing and are prohibited
by DNS standards.
Reference: https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2
'''
[docs]
def validate(self, zone):
reasons = []
targets = {
r.fqdn: r for r in zone.records if r._type in ('CNAME', 'ALIAS')
}
overall_visited = set()
for start_fqdn in targets:
if start_fqdn in overall_visited:
continue
path = []
visited = {} # fqdn -> index in path
curr = start_fqdn
while curr in targets:
if curr in visited:
cycle_fqdns = path[visited[curr] :] + [curr]
loop_path = ' -> '.join(cycle_fqdns)
cycle_records = {
targets[f] for f in cycle_fqdns if f in targets
}
reasons.append(
ValidationReason(
f'Loop detected: {loop_path}', cycle_records
)
)
break
if curr in overall_visited:
break
visited[curr] = len(path)
path.append(curr)
overall_visited.add(curr)
curr = str(targets[curr].value)
return reasons
[docs]
class CnameTargetResolvableInZoneZoneValidator(ZoneValidator):
'''
Checks that ``CNAME`` and ``ALIAS`` records pointing to targets within the
same zone have a corresponding record at that target. This helps detect
"dangling" references that can occur after refactors or deletions.
'''
[docs]
def validate(self, zone):
reasons = []
for record in zone.records:
if record._type in ('CNAME', 'ALIAS'):
target = str(record.value)
if zone.owns('A', target):
hostname = zone.hostname_from_fqdn(target)
if not zone.get(hostname):
reasons.append(
ValidationReason(
f'{record._type} record "{record.decoded_fqdn}" points to in-zone target "{target}" that does not exist',
[record],
)
)
return reasons
[docs]
class RootCnameZoneValidator(ZoneValidator):
'''
Checks that a CNAME record is not present at the zone apex (root).
RFC 1912 forbids CNAME records at the root.
'''
[docs]
def validate(self, zone):
reasons = []
cname = zone.get_type('', 'CNAME')
if cname:
reasons.append(
ValidationReason(
f'CNAME record at zone apex "{cname.decoded_fqdn}" is not allowed',
[cname],
)
)
return reasons
[docs]
class CnameTargetNotCnameZoneValidator(ZoneValidator):
'''
Checks that CNAME records do not point to other CNAME records within the same zone.
'''
[docs]
def validate(self, zone):
reasons = []
for record in zone.records:
if record._type == 'CNAME':
target = str(record.value)
if zone.owns('CNAME', target):
hostname = zone.hostname_from_fqdn(target)
cnames = zone.get(hostname, type='CNAME')
if cnames:
reasons.append(
ValidationReason(
f'CNAME record "{record.decoded_fqdn}" points to target "{target}" which is also a CNAME',
[record],
)
)
return reasons
Zone.register_zone_validator(
CnameCoexistenceValidator('cname-coexistence', sets={'legacy', 'strict'})
)
Zone.register_zone_validator(
NoCnameLoopZoneValidator('no-cname-loop', sets={'strict'})
)
Zone.register_zone_validator(
CnameTargetResolvableInZoneZoneValidator(
'cname-target-resolvable-in-zone', sets={'best-practice'}
)
)
Zone.register_zone_validator(
RootCnameZoneValidator('root-cname', sets={'strict'})
)
Zone.register_zone_validator(
CnameTargetNotCnameZoneValidator(
'cname-target-not-cname', sets={'best-practice'}
)
)