fix(lookup): keep the clearnet apex redirect on a multi-network node

current_play_redirect_domains gated both apex entries on web-opt-rdr-domains being in lookup('deployment').deployed whenever the host serves Tor. On a multi node that also dropped the clearnet DOMAIN_PRIMARY -> DOMAIN_HOMEPAGE entry for every isolated variant without that role, so the apex had no vhost and answered with a TLS handshake reject; the Matrix federation delegation on the apex depends on that vhost in swarm. A clearnet node, and main, always schedules it.

The clearnet entry is now scheduled whenever the node's resolved NETWORK_MODE serves clearnet, and the deployment gate keeps guarding only the onion-apex entry. A tor-only node keeps the old behaviour, and the operator's production inventory runs no Tor host, so production is unaffected. Tests cover multi without the role (clearnet apex only) and tor-only (no apex).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-24 16:16:38 +02:00
1 parent 6de71db3d0
commit e4fd4552f0
2 files changed
+65 -19

No files matched your search

+46 -19
View File
@@ -10,7 +10,14 @@ from ansible.plugins.lookup import LookupBase
from plugins.filter.merge.mapping import merge_mapping
from utils.domains.list import render_domain_value
from utils.networks.reachability import TOR, network_of, tor_serves_host
from utils.networks.reachability import (
CLEARNET,
NODE_MODES,
TOR,
network_of,
resolve_node_mode,
tor_serves_host,
)
from utils.roles.entity.name import entity_name
@@ -133,9 +140,11 @@ class LookupModule(LookupBase):
is an onion host or has an onion sibling this node serves, which
the redirect template sends the apex to; a homepage without one
gets no onion apex entry, so an onion vhost never redirects to
clearnet. Both entries require ``web-opt-rdr-domains`` in
``lookup('deployment').deployed`` so isolated CI variants do not
schedule a redirect whose target is not served.
clearnet. On an onion node the onion-apex entry requires
``web-opt-rdr-domains`` in ``lookup('deployment').deployed`` so
isolated CI variants do not schedule a redirect whose target is not
served; the clearnet entry needs it only when the node's
``NETWORK_MODE`` does not serve clearnet.
3. per-app alias-to-canonical mappings from
``lookup('applications_current_play')``, gated by ``AUTO_BUILD_ALIASES``.
@@ -168,6 +177,22 @@ class LookupModule(LookupBase):
node = tor.get("node") if isinstance(tor, Mapping) else None
return self._resolve_str(node).strip()
def _serves_clearnet(self, vars_: Mapping[str, Any]) -> bool:
"""Whether the node's resolved ``NETWORK_MODE`` includes clearnet.
Args:
vars_: host variables carrying ``NETWORK_MODE``, ``group_names``
and ``groups``.
"""
try:
mode = resolve_node_mode(
self._resolve_str(vars_.get("NETWORK_MODE")),
tor_provided=tor_serves_host(vars_),
)
except ValueError as exc:
raise AnsibleError(f"current_play_redirect_domains: {exc}") from exc
return CLEARNET in NODE_MODES[mode]
def _reachable_on_onion(self, domain: str, vars_: Mapping[str, Any]) -> bool:
"""Whether ``domain`` is an onion host or has an onion sibling this
node serves.
@@ -239,21 +264,23 @@ class LookupModule(LookupBase):
else []
)
allowed = "web-opt-rdr-domains" in deployed
if allowed:
primary_redirects = []
if domain_primary != domain_homepage:
primary_redirects.append(
{"source": domain_primary, "target": domain_homepage}
)
if (
node_onion
and node_onion != domain_homepage
and self._reachable_on_onion(domain_homepage, vars_)
):
primary_redirects.append(
{"source": node_onion, "target": domain_homepage}
)
merged = merge_mapping(merged, primary_redirects, "source")
primary_redirects = []
if domain_primary != domain_homepage and (
allowed or self._serves_clearnet(vars_)
):
primary_redirects.append(
{"source": domain_primary, "target": domain_homepage}
)
if (
allowed
and node_onion
and node_onion != domain_homepage
and self._reachable_on_onion(domain_homepage, vars_)
):
primary_redirects.append(
{"source": node_onion, "target": domain_homepage}
)
merged = merge_mapping(merged, primary_redirects, "source")
current_play_apps = self._run_lookup("applications_current_play", vars_)
if isinstance(current_play_apps, Mapping):
@@ -403,6 +403,25 @@ class CurrentPlayRedirectDomainsLookupTests(unittest.TestCase):
def test_onion_apex_absent_when_rdr_domains_not_deployed(self):
lm = self._make_lookup()
variables = self._onion_variables()
with self._patch_lookups(deployed=["web-opt-rdr-www"], current_play_apps={}):
result = lm.run(terms=[], variables=variables)[0]
self.assertEqual(
result,
[{"source": DOMAIN, "target": "dashboard.abcdefonion.onion"}],
)
def test_multi_node_keeps_the_clearnet_apex_without_rdr_domains_deployed(self):
lm = self._make_lookup()
variables = self._onion_variables(
NETWORK_MODE="multi", DOMAIN_HOMEPAGE="infinito.nexus"
)
with self._patch_lookups(deployed=["web-opt-rdr-www"], current_play_apps={}):
result = lm.run(terms=[], variables=variables)[0]
self.assertEqual(result, [{"source": DOMAIN, "target": "infinito.nexus"}])
def test_tor_only_node_drops_both_apex_entries_without_rdr_domains_deployed(self):
lm = self._make_lookup()
variables = self._onion_variables(NETWORK_MODE="tor")
with self._patch_lookups(deployed=["web-opt-rdr-www"], current_play_apps={}):
result = lm.run(terms=[], variables=variables)[0]
self.assertEqual(result, [])