From ce8958cc01c673aa0f700492f8759a315ea35141 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 12 Sep 2025 03:47:37 +0200 Subject: [PATCH] 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 --- roles/sys-dns-wildcards/README.md | 6 +- roles/sys-dns-wildcards/defaults/main.yml | 2 - .../filter_plugins/wildcard_dns.py | 41 +++++++++---- roles/sys-dns-wildcards/vars/main.yml | 20 +++---- .../filter_plugins/test_wildcard_dns.py | 60 +++++++++++++++---- 5 files changed, 90 insertions(+), 39 deletions(-) diff --git a/roles/sys-dns-wildcards/README.md b/roles/sys-dns-wildcards/README.md index b045f9b2..d961d268 100644 --- a/roles/sys-dns-wildcards/README.md +++ b/roles/sys-dns-wildcards/README.md @@ -1,19 +1,17 @@ # sys-dns-wildcards -Create Cloudflare DNS **wildcard** A/AAAA records (`*.parent`) only for **parent hosts** (hosts that have children). -The **apex** (SLD.TLD) is considered when computing parents, but **no base host** or `*.apex` record is created by this role. +Create Cloudflare DNS **wildcard** A/AAAA records (`*.parent`) for **parent hosts** (hosts that have children) **and** always for the **apex** (SLD.TLD). Examples: - c.wiki.example.com -> parent: wiki.example.com -> creates: `*.wiki.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 - 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 - Flags: - parent_dns_enabled (bool, default: true) - - parent_dns_ipv6_enabled (bool, default: true) - parent_dns_proxied (bool, default: false) ## Usage diff --git a/roles/sys-dns-wildcards/defaults/main.yml b/roles/sys-dns-wildcards/defaults/main.yml index 3c3276a2..17696a1a 100644 --- a/roles/sys-dns-wildcards/defaults/main.yml +++ b/roles/sys-dns-wildcards/defaults/main.yml @@ -1,3 +1 @@ parent_dns_proxied: false -parent_dns_domains: [] -parent_dns_ipv6_enabled: true diff --git a/roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py b/roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py index 8a4a86f0..9fc6c94b 100644 --- a/roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py +++ b/roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py @@ -1,3 +1,4 @@ +# roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py from ansible.errors import AnsibleFilterError import ipaddress @@ -15,7 +16,9 @@ def _depth(domain: str, apex: str) -> int: 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): return None 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]: """ - Accepts CURRENT_PLAY_DOMAINS_ALL-like structures: + Accept CURRENT_PLAY_DOMAINS*_like structures: - dict values: str | list/tuple/set[str] | dict (one level deeper) Returns unique, sorted host list. """ @@ -100,18 +103,24 @@ def _build_wildcard_records( records.append({ "zone": apex, "type": rtype, - "name": name, + "name": name, # For apex wildcard, name "*" means "*.apex" in Cloudflare "content": content, "proxied": bool(proxied), "ttl": 1, }) for p in sorted(set(parents)): - rel = p[:-len(apex)-1] if p != apex else "" # relative part; apex shouldn't produce wildcard - if not rel: - # Do NOT create *.apex here (explicitly excluded by requirement) - continue - wc = f"*.{rel}" + # Create wildcard at apex as well (name="*") + if p == apex: + wc = "*" + else: + # 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)) if ipv6_enabled and ip6 and _is_global(str(ip6)): _add(wc, "AAAA", str(ip6)) @@ -119,7 +128,7 @@ def _build_wildcard_records( def wildcard_records( - current_play_domains_all: dict, + current_play_domains_all, # dict expected when explicit_domains is None apex: str, ip4: str, ip6: str | None = None, @@ -129,17 +138,25 @@ def wildcard_records( ipv6_enabled: bool = True, ) -> list[dict]: """ - Build only wildcard records for parents: - for each parent 'parent.apex' -> create '*.parent' A/AAAA. - No base parent records and no '*.apex' are created. + Build wildcard records: + - for each parent 'parent.apex' -> create '*.parent' A/AAAA + - 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 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)) else: 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 = list(set(parents) | {apex}) + return _build_wildcard_records( parents, apex, diff --git a/roles/sys-dns-wildcards/vars/main.yml b/roles/sys-dns-wildcards/vars/main.yml index abaf7e67..671e9619 100644 --- a/roles/sys-dns-wildcards/vars/main.yml +++ b/roles/sys-dns-wildcards/vars/main.yml @@ -1,12 +1,12 @@ SYN_DNS_WILDCARD_RECORDS: >- - {{ CURRENT_PLAY_DOMAINS_ALL - | wildcard_records( - PRIMARY_DOMAIN, - defaults_networks.internet.ip4, - (defaults_networks.internet.ip6 | default('')), - parent_dns_proxied, - (parent_dns_domains | default([])), - 2, - parent_dns_ipv6_enabled - ) + {{ + {} | + wildcard_records( + PRIMARY_DOMAIN, + networks.internet.ip4, + networks.internet.ip6, + parent_dns_proxied, + explicit_domains=CURRENT_PLAY_DOMAINS_ALL, + min_child_depth=2, + ipv6_enabled=true) }} diff --git a/tests/unit/roles/sys-dns-wildcards/filter_plugins/test_wildcard_dns.py b/tests/unit/roles/sys-dns-wildcards/filter_plugins/test_wildcard_dns.py index 0bc1e14b..0db6dc8a 100644 --- a/tests/unit/roles/sys-dns-wildcards/filter_plugins/test_wildcard_dns.py +++ b/tests/unit/roles/sys-dns-wildcards/filter_plugins/test_wildcard_dns.py @@ -48,7 +48,7 @@ class TestWildcardDNS(unittest.TestCase): def setUp(self): self.wildcard_records = _get_filter() - def test_only_wildcards_no_apex_or_base(self): + def test_only_wildcards_including_apex(self): apex = "example.com" cpda = { "svc-a": ["c.wiki.example.com", "a.b.example.com"], @@ -69,19 +69,24 @@ class TestWildcardDNS(unittest.TestCase): got = _as_set(recs) 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), ("AAAA", "*.wiki", "2606:4700:4700::1111", True), ("A", "*.b", "203.0.113.10", 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), ("AAAA", "*.a.b", "2606:4700:4700::1111", True), } 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" - cpda = {"svc": ["x.example.com"]} # depth = 1 + cpda = {"svc": ["x.example.com"]} # depth = 1, below threshold recs = self.wildcard_records( current_play_domains_all=cpda, @@ -93,13 +98,18 @@ class TestWildcardDNS(unittest.TestCase): min_child_depth=2, # requires >= 2 → no parent derived 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): apex = "example.com" cpda = {"svc": ["a.b.example.com"]} - # IPv6 disabled → only A record + # IPv6 disabled → only A records (apex + parent) recs1 = self.wildcard_records( current_play_domains_all=cpda, apex=apex, @@ -110,9 +120,15 @@ class TestWildcardDNS(unittest.TestCase): min_child_depth=2, 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( current_play_domains_all=cpda, apex=apex, @@ -123,7 +139,13 @@ class TestWildcardDNS(unittest.TestCase): min_child_depth=2, 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): recs = self.wildcard_records( @@ -137,7 +159,13 @@ class TestWildcardDNS(unittest.TestCase): ipv6_enabled=True, ) 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): cpda = {"svc": ["ignore.me.example.com", "a.b.example.com"]} @@ -156,6 +184,11 @@ class TestWildcardDNS(unittest.TestCase): self.assertEqual( _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), ("AAAA", "*.wiki", "2606:4700:4700::1111", False), }, @@ -183,11 +216,16 @@ class TestWildcardDNS(unittest.TestCase): ) got = _as_set(recs) 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), ("AAAA", "*.wiki", "2606:4700:4700::1111", False), ("A", "*.b", "203.0.113.21", 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), ("AAAA", "*.a.b", "2606:4700:4700::1111", False), }