diff --git a/plugins/lookup/network_redirects.py b/plugins/lookup/network_redirects.py index 89f7a0cb21..44e3095801 100644 --- a/plugins/lookup/network_redirects.py +++ b/plugins/lookup/network_redirects.py @@ -8,6 +8,8 @@ from ansible.errors import AnsibleError from ansible.plugins.loader import lookup_loader from ansible.plugins.lookup import LookupBase +from utils.networks.reachability import network_of + class LookupModule(LookupBase): """ @@ -16,8 +18,10 @@ class LookupModule(LookupBase): Returns one ``{source, target}`` mapping per network sibling of each source, the first mapping of a source winning. A source without siblings - is kept as given. Targets stay canonical; the redirect template moves - each one into the network of its source. + is kept as given, and so is the canonical sibling of a source; a further + sibling is kept only where its target is served in that sibling's network, + so no vhost redirects into another network. Targets stay canonical; the + redirect template moves each one into the network of its source. """ def run(self, terms, variables: dict[str, Any] | None = None, **kwargs): @@ -36,9 +40,18 @@ class LookupModule(LookupBase): f"network_redirects: expected {{source, target}} mappings, got {mapping!r}" ) source = str(mapping["source"]) - for sibling in siblings.run([source], variables=variables)[0] or [source]: + target = str(mapping.get("target") or "") + target_networks = { + network_of(host) + for host in siblings.run([target], variables=variables)[0] or [target] + } + for index, sibling in enumerate( + siblings.run([source], variables=variables)[0] or [source] + ): if sibling in seen: continue + if index and network_of(sibling) not in target_networks: + continue seen.add(sibling) fanned.append({**mapping, "source": sibling}) return [fanned] diff --git a/tests/unit/python/plugins/lookup/test_network_redirects.py b/tests/unit/python/plugins/lookup/test_network_redirects.py index fdf5a94ba0..4ae51d5a66 100644 --- a/tests/unit/python/plugins/lookup/test_network_redirects.py +++ b/tests/unit/python/plugins/lookup/test_network_redirects.py @@ -10,6 +10,8 @@ PRIMARY = default_domain_primary() ONION = "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvw.onion" SIBLINGS = { f"css.{PRIMARY}": [f"css.{PRIMARY}", f"css.{ONION}"], + f"cdn.{PRIMARY}": [f"cdn.{PRIMARY}", f"cdn.{ONION}"], + f"apex.{PRIMARY}": [f"apex.{PRIMARY}", f"apex.{ONION}"], ONION: [], } @@ -40,6 +42,15 @@ class TestNetworkRedirects(unittest.TestCase): ], ) + def test_a_sibling_whose_network_the_target_does_not_reach_is_dropped(self): + target = f"dashboard.{PRIMARY}" + self.assertEqual( + self._run([{"source": f"apex.{PRIMARY}", "target": target}]), + [{"source": f"apex.{PRIMARY}", "target": target}], + "an onion sibling redirecting to a clearnet-only target would send " + "a Tor visitor off the onion", + ) + def test_a_source_without_siblings_is_kept(self): mappings = [ {"source": PRIMARY, "target": f"home.{PRIMARY}"},