sys-dns-wildcards: always create apex wildcard (*.apex); use explicit_domains for CURRENT_PLAY_DOMAINS_ALL list; update README and unit tests. Ref: https://chatgpt.com/share/68c37a74-7468-800f-a612-765bbbd442de

This commit is contained in:
2025-09-12 03:47:37 +02:00
parent 7e5990aa16
commit ce8958cc01
5 changed files with 90 additions and 39 deletions

View File

@@ -1,19 +1,17 @@
# sys-dns-wildcards # sys-dns-wildcards
Create Cloudflare DNS **wildcard** A/AAAA records (`*.parent`) only for **parent hosts** (hosts that have children). Create Cloudflare DNS **wildcard** A/AAAA records (`*.parent`) for **parent hosts** (hosts that have children) **and** always for the **apex** (SLD.TLD).
The **apex** (SLD.TLD) is considered when computing parents, but **no base host** or `*.apex` record is created by this role.
Examples: Examples:
- c.wiki.example.com -> parent: wiki.example.com -> creates: `*.wiki.example.com` - c.wiki.example.com -> parent: wiki.example.com -> creates: `*.wiki.example.com`
- a.b.example.com -> parent: b.example.com -> creates: `*.b.example.com` - a.b.example.com -> parent: b.example.com -> creates: `*.b.example.com`
- example.com (apex) -> used to detect parents, but **no** `example.com` or `*.example.com` record is created - example.com (apex) -> also creates: `*.example.com`
## Inputs ## Inputs
- parent_dns_domains (list[str], optional): FQDNs to evaluate. If empty, the role flattens CURRENT_PLAY_DOMAINS_ALL. - parent_dns_domains (list[str], optional): FQDNs to evaluate. If empty, the role flattens CURRENT_PLAY_DOMAINS_ALL.
- PRIMARY_DOMAIN (apex), defaults_networks.internet.ip4, optional defaults_networks.internet.ip6 - PRIMARY_DOMAIN (apex), defaults_networks.internet.ip4, optional defaults_networks.internet.ip6
- Flags: - Flags:
- parent_dns_enabled (bool, default: true) - parent_dns_enabled (bool, default: true)
- parent_dns_ipv6_enabled (bool, default: true)
- parent_dns_proxied (bool, default: false) - parent_dns_proxied (bool, default: false)
## Usage ## Usage

View File

@@ -1,3 +1 @@
parent_dns_proxied: false parent_dns_proxied: false
parent_dns_domains: []
parent_dns_ipv6_enabled: true

View File

@@ -1,3 +1,4 @@
# roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
import ipaddress import ipaddress
@@ -15,7 +16,9 @@ def _depth(domain: str, apex: str) -> int:
def _parent_of_child(domain: str, apex: str) -> str | None: def _parent_of_child(domain: str, apex: str) -> str | None:
"""For a child like a.b.example.com return b.example.com; else None (needs depth >= 2).""" """
For a child like a.b.example.com return b.example.com; else None (needs depth >= 2).
"""
if not domain.endswith(apex): if not domain.endswith(apex):
return None return None
parts = domain.split(".") parts = domain.split(".")
@@ -27,7 +30,7 @@ def _parent_of_child(domain: str, apex: str) -> str | None:
def _flatten_domains_any_structure(domains_like) -> list[str]: def _flatten_domains_any_structure(domains_like) -> list[str]:
""" """
Accepts CURRENT_PLAY_DOMAINS_ALL-like structures: Accept CURRENT_PLAY_DOMAINS*_like structures:
- dict values: str | list/tuple/set[str] | dict (one level deeper) - dict values: str | list/tuple/set[str] | dict (one level deeper)
Returns unique, sorted host list. Returns unique, sorted host list.
""" """
@@ -100,18 +103,24 @@ def _build_wildcard_records(
records.append({ records.append({
"zone": apex, "zone": apex,
"type": rtype, "type": rtype,
"name": name, "name": name, # For apex wildcard, name "*" means "*.apex" in Cloudflare
"content": content, "content": content,
"proxied": bool(proxied), "proxied": bool(proxied),
"ttl": 1, "ttl": 1,
}) })
for p in sorted(set(parents)): for p in sorted(set(parents)):
rel = p[:-len(apex)-1] if p != apex else "" # relative part; apex shouldn't produce wildcard # Create wildcard at apex as well (name="*")
if not rel: if p == apex:
# Do NOT create *.apex here (explicitly excluded by requirement) wc = "*"
continue else:
wc = f"*.{rel}" # relative part (drop ".apex")
rel = p[:-len(apex)-1]
if not rel:
# Safety guard; should not happen because p==apex handled above
wc = "*"
else:
wc = f"*.{rel}"
_add(wc, "A", str(ip4)) _add(wc, "A", str(ip4))
if ipv6_enabled and ip6 and _is_global(str(ip6)): if ipv6_enabled and ip6 and _is_global(str(ip6)):
_add(wc, "AAAA", str(ip6)) _add(wc, "AAAA", str(ip6))
@@ -119,7 +128,7 @@ def _build_wildcard_records(
def wildcard_records( def wildcard_records(
current_play_domains_all: dict, current_play_domains_all, # dict expected when explicit_domains is None
apex: str, apex: str,
ip4: str, ip4: str,
ip6: str | None = None, ip6: str | None = None,
@@ -129,17 +138,25 @@ def wildcard_records(
ipv6_enabled: bool = True, ipv6_enabled: bool = True,
) -> list[dict]: ) -> list[dict]:
""" """
Build only wildcard records for parents: Build wildcard records:
for each parent 'parent.apex' -> create '*.parent' A/AAAA. - for each parent 'parent.apex' -> create '*.parent' A/AAAA
No base parent records and no '*.apex' are created. - ALWAYS also create '*.apex' (apex wildcard), modeled as name="*"
Sources:
- If 'explicit_domains' is provided and non-empty, use it (expects list[str]).
- Else flatten 'current_play_domains_all' (expects dict).
""" """
# Source domains # Source domains
if explicit_domains and len(explicit_domains) > 0: if explicit_domains and len(explicit_domains) > 0:
if not isinstance(explicit_domains, list) or not all(isinstance(x, str) for x in explicit_domains):
raise AnsibleFilterError("explicit_domains must be list[str]")
domains = sorted(set(explicit_domains)) domains = sorted(set(explicit_domains))
else: else:
domains = _flatten_domains_any_structure(current_play_domains_all) domains = _flatten_domains_any_structure(current_play_domains_all)
# Determine parents and ALWAYS include apex for apex wildcard
parents = _parents_from(domains, apex, min_child_depth=min_child_depth) parents = _parents_from(domains, apex, min_child_depth=min_child_depth)
parents = list(set(parents) | {apex})
return _build_wildcard_records( return _build_wildcard_records(
parents, parents,
apex, apex,

View File

@@ -1,12 +1,12 @@
SYN_DNS_WILDCARD_RECORDS: >- SYN_DNS_WILDCARD_RECORDS: >-
{{ CURRENT_PLAY_DOMAINS_ALL {{
| wildcard_records( {} |
PRIMARY_DOMAIN, wildcard_records(
defaults_networks.internet.ip4, PRIMARY_DOMAIN,
(defaults_networks.internet.ip6 | default('')), networks.internet.ip4,
parent_dns_proxied, networks.internet.ip6,
(parent_dns_domains | default([])), parent_dns_proxied,
2, explicit_domains=CURRENT_PLAY_DOMAINS_ALL,
parent_dns_ipv6_enabled min_child_depth=2,
) ipv6_enabled=true)
}} }}

View File

@@ -48,7 +48,7 @@ class TestWildcardDNS(unittest.TestCase):
def setUp(self): def setUp(self):
self.wildcard_records = _get_filter() self.wildcard_records = _get_filter()
def test_only_wildcards_no_apex_or_base(self): def test_only_wildcards_including_apex(self):
apex = "example.com" apex = "example.com"
cpda = { cpda = {
"svc-a": ["c.wiki.example.com", "a.b.example.com"], "svc-a": ["c.wiki.example.com", "a.b.example.com"],
@@ -69,19 +69,24 @@ class TestWildcardDNS(unittest.TestCase):
got = _as_set(recs) got = _as_set(recs)
expected = { expected = {
# apex wildcard always
("A", "*", "203.0.113.10", True),
("AAAA", "*", "2606:4700:4700::1111", True),
# derived parents
("A", "*.wiki", "203.0.113.10", True), ("A", "*.wiki", "203.0.113.10", True),
("AAAA", "*.wiki", "2606:4700:4700::1111", True), ("AAAA", "*.wiki", "2606:4700:4700::1111", True),
("A", "*.b", "203.0.113.10", True), ("A", "*.b", "203.0.113.10", True),
("AAAA", "*.b", "2606:4700:4700::1111", True), ("AAAA", "*.b", "2606:4700:4700::1111", True),
# now included because www.a.b.example.com promotes a.b.example.com as a parent # www.a.b.example.com promotes a.b.example.com as a parent
("A", "*.a.b", "203.0.113.10", True), ("A", "*.a.b", "203.0.113.10", True),
("AAAA", "*.a.b", "2606:4700:4700::1111", True), ("AAAA", "*.a.b", "2606:4700:4700::1111", True),
} }
self.assertEqual(got, expected) self.assertEqual(got, expected)
def test_min_child_depth_prevents_apex_wildcard(self): def test_min_child_depth_yields_only_apex(self):
apex = "example.com" apex = "example.com"
cpda = {"svc": ["x.example.com"]} # depth = 1 cpda = {"svc": ["x.example.com"]} # depth = 1, below threshold
recs = self.wildcard_records( recs = self.wildcard_records(
current_play_domains_all=cpda, current_play_domains_all=cpda,
@@ -93,13 +98,18 @@ class TestWildcardDNS(unittest.TestCase):
min_child_depth=2, # requires >= 2 → no parent derived min_child_depth=2, # requires >= 2 → no parent derived
ipv6_enabled=True, ipv6_enabled=True,
) )
self.assertEqual(recs, []) got = _as_set(recs)
expected = {
("A", "*", "198.51.100.42", False),
("AAAA", "*", "2606:4700:4700::1111", False),
}
self.assertEqual(got, expected)
def test_ipv6_disabled_and_private_ipv6_filtered(self): def test_ipv6_disabled_and_private_ipv6_filtered(self):
apex = "example.com" apex = "example.com"
cpda = {"svc": ["a.b.example.com"]} cpda = {"svc": ["a.b.example.com"]}
# IPv6 disabled → only A record # IPv6 disabled → only A records (apex + parent)
recs1 = self.wildcard_records( recs1 = self.wildcard_records(
current_play_domains_all=cpda, current_play_domains_all=cpda,
apex=apex, apex=apex,
@@ -110,9 +120,15 @@ class TestWildcardDNS(unittest.TestCase):
min_child_depth=2, min_child_depth=2,
ipv6_enabled=False, ipv6_enabled=False,
) )
self.assertEqual(_as_set(recs1), {("A", "*.b", "203.0.113.9", False)}) self.assertEqual(
_as_set(recs1),
{
("A", "*", "203.0.113.9", False),
("A", "*.b", "203.0.113.9", False),
},
)
# IPv6 enabled but ULA (not global) → skip AAAA # IPv6 enabled but ULA (not global) → skip AAAA (apex + parent)
recs2 = self.wildcard_records( recs2 = self.wildcard_records(
current_play_domains_all=cpda, current_play_domains_all=cpda,
apex=apex, apex=apex,
@@ -123,7 +139,13 @@ class TestWildcardDNS(unittest.TestCase):
min_child_depth=2, min_child_depth=2,
ipv6_enabled=True, ipv6_enabled=True,
) )
self.assertEqual(_as_set(recs2), {("A", "*.b", "203.0.113.9", False)}) self.assertEqual(
_as_set(recs2),
{
("A", "*", "203.0.113.9", False),
("A", "*.b", "203.0.113.9", False),
},
)
def test_proxied_flag_true_is_set(self): def test_proxied_flag_true_is_set(self):
recs = self.wildcard_records( recs = self.wildcard_records(
@@ -137,7 +159,13 @@ class TestWildcardDNS(unittest.TestCase):
ipv6_enabled=True, ipv6_enabled=True,
) )
self.assertTrue(all(r.get("proxied") is True for r in recs)) self.assertTrue(all(r.get("proxied") is True for r in recs))
self.assertEqual(_as_set(recs), {("A", "*.b", "203.0.113.7", True)}) self.assertEqual(
_as_set(recs),
{
("A", "*", "203.0.113.7", True),
("A", "*.b", "203.0.113.7", True),
},
)
def test_explicit_domains_override_source(self): def test_explicit_domains_override_source(self):
cpda = {"svc": ["ignore.me.example.com", "a.b.example.com"]} cpda = {"svc": ["ignore.me.example.com", "a.b.example.com"]}
@@ -156,6 +184,11 @@ class TestWildcardDNS(unittest.TestCase):
self.assertEqual( self.assertEqual(
_as_set(recs), _as_set(recs),
{ {
# apex wildcard always
("A", "*", "203.0.113.5", False),
("AAAA", "*", "2606:4700:4700::1111", False),
# derived from explicit domain
("A", "*.wiki", "203.0.113.5", False), ("A", "*.wiki", "203.0.113.5", False),
("AAAA", "*.wiki", "2606:4700:4700::1111", False), ("AAAA", "*.wiki", "2606:4700:4700::1111", False),
}, },
@@ -183,11 +216,16 @@ class TestWildcardDNS(unittest.TestCase):
) )
got = _as_set(recs) got = _as_set(recs)
expected = { expected = {
# apex wildcard always
("A", "*", "203.0.113.21", False),
("AAAA", "*", "2606:4700:4700::1111", False),
# derived parents
("A", "*.wiki", "203.0.113.21", False), ("A", "*.wiki", "203.0.113.21", False),
("AAAA", "*.wiki", "2606:4700:4700::1111", False), ("AAAA", "*.wiki", "2606:4700:4700::1111", False),
("A", "*.b", "203.0.113.21", False), ("A", "*.b", "203.0.113.21", False),
("AAAA", "*.b", "2606:4700:4700::1111", False), ("AAAA", "*.b", "2606:4700:4700::1111", False),
# now included because www.a.b.example.com promotes a.b.example.com as a parent # www.a.b.example.com promotes a.b.example.com as a parent
("A", "*.a.b", "203.0.113.21", False), ("A", "*.a.b", "203.0.113.21", False),
("AAAA", "*.a.b", "2606:4700:4700::1111", False), ("AAAA", "*.a.b", "2606:4700:4700::1111", False),
} }