mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-14 06:17:14 +02:00
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:
@@ -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
|
||||||
|
@@ -1,3 +1 @@
|
|||||||
parent_dns_proxied: false
|
parent_dns_proxied: false
|
||||||
parent_dns_domains: []
|
|
||||||
parent_dns_ipv6_enabled: true
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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)
|
||||||
}}
|
}}
|
||||||
|
@@ -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),
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user