Files
computer-playbook/tests/unit/python/utils/networks/test_reachability.py
T
kevinveenbirkenbachandClaude Opus 5.5 66c8df6729 feat(network): serve roles on clearnet and onion from one registry
A node runs in NETWORK_MODE clearnet, tor or multi; empty derives multi when svc-net-tor is deployed. meta/networks.yml reachability narrows a role (modes, single_mode, each justified by a network-reachability nocheck), and utils/networks/reachability.py is the one registry every network decision goes through. services.tor keeps only the dependency bond; exclusive and primary are gone and Matrix is single_mode.

Every canonical domain gains one sibling per effective network, clearnet first. sys-stk-front-proxy renders one vhost per sibling, and TLS, CSP, injected snippets, redirects, the web health probe and the CSP crawler are decided per vhost. Cross-app URLs follow the network of the vhost being rendered, the SSO issuer stays single, clearnet vhosts of multi roles send Onion-Location, and the shared location forwards X-Forwarded-Host, which lets Wazuh derive its OIDC redirect per request again. The to_onion_url filter is removed; its two consumers render per vhost instead.

Measured before commit: on a clearnet node the domain map of all 108 apps and every CSP header are identical to HEAD; on a multi node every role keeps its clearnet domains first and only gains onion siblings, and no CSP source crosses networks except the SSO issuer.

Refs: docs/requirements/037-multi-network-serving.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 19:42:05 +02:00

172 lines
5.8 KiB
Python

"""Contract of the network registry and the per-role reachability resolution."""
from __future__ import annotations
import unittest
from utils.networks.reachability import (
CLEARNET,
MULTI,
TOR,
allowed_networks,
effective_networks,
is_network,
network,
network_of,
resolve_node_mode,
role_reachability,
sibling_domain,
)
ONION = "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvw.onion"
def _cfg(**reachability) -> dict:
return {"networks": {"reachability": reachability}} if reachability else {}
class TestNetworkOf(unittest.TestCase):
def test_onion_suffix_is_tor(self) -> None:
self.assertEqual(network_of(f"wazuh.{ONION}"), TOR)
def test_trailing_dot_and_case_are_ignored(self) -> None:
self.assertEqual(network_of(f"Wazuh.{ONION.upper()}."), TOR)
def test_everything_else_is_clearnet(self) -> None:
self.assertEqual(network_of("wazuh.infinito.test"), CLEARNET)
def test_is_network(self) -> None:
self.assertTrue(is_network(ONION, TOR))
self.assertFalse(is_network("infinito.test", TOR))
def test_reserved_network_is_refused(self) -> None:
with self.assertRaises(ValueError):
network("handshake")
def test_unknown_network_is_refused(self) -> None:
with self.assertRaises(ValueError):
network("i2p")
class TestSiblingDomain(unittest.TestCase):
def test_clearnet_subdomain_to_tor(self) -> None:
self.assertEqual(
sibling_domain("wazuh.infinito.test", TOR, "infinito.test", ONION),
f"wazuh.{ONION}",
)
def test_apex_to_tor_is_the_node_onion(self) -> None:
self.assertEqual(
sibling_domain("infinito.test", TOR, "infinito.test", ONION), ONION
)
def test_tor_subdomain_to_clearnet(self) -> None:
self.assertEqual(
sibling_domain(f"wazuh.{ONION}", CLEARNET, "infinito.test", ONION),
"wazuh.infinito.test",
)
def test_same_network_returns_itself(self) -> None:
self.assertEqual(
sibling_domain("wazuh.infinito.test", CLEARNET, "infinito.test", ONION),
"wazuh.infinito.test",
)
def test_foreign_domain_has_no_sibling(self) -> None:
self.assertIsNone(sibling_domain("example.org", TOR, "infinito.test", ONION))
def test_no_node_address_has_no_sibling(self) -> None:
self.assertIsNone(
sibling_domain("wazuh.infinito.test", TOR, "infinito.test", "")
)
class TestResolveNodeMode(unittest.TestCase):
def test_empty_derives_multi_with_tor(self) -> None:
self.assertEqual(resolve_node_mode("", tor_provided=True), MULTI)
def test_empty_derives_clearnet_without_tor(self) -> None:
self.assertEqual(resolve_node_mode(None, tor_provided=False), CLEARNET)
def test_explicit_tor(self) -> None:
self.assertEqual(resolve_node_mode("tor", tor_provided=True), TOR)
def test_tor_mode_without_provider_fails(self) -> None:
with self.assertRaises(ValueError):
resolve_node_mode("multi", tor_provided=False)
def test_unknown_mode_fails(self) -> None:
with self.assertRaises(ValueError):
resolve_node_mode("dual", tor_provided=True)
class TestRoleReachability(unittest.TestCase):
def test_missing_means_every_network(self) -> None:
self.assertEqual(role_reachability({}), ((), False))
def test_empty_list_means_every_network(self) -> None:
self.assertEqual(role_reachability(_cfg(modes=[])), ((), False))
def test_multi_is_not_a_list_value(self) -> None:
with self.assertRaises(ValueError):
role_reachability(_cfg(modes=[CLEARNET, MULTI]))
def test_single_mode_must_be_boolean(self) -> None:
with self.assertRaises(TypeError):
role_reachability(_cfg(single_mode="yes"))
def test_tor_disabled_role_loses_tor(self) -> None:
self.assertEqual(allowed_networks({}, tor_enabled=False), ((CLEARNET,), False))
class TestEffectiveNetworks(unittest.TestCase):
"""Every combination of node mode, ``modes`` and ``single_mode``."""
CASES = (
(CLEARNET, (), False, (CLEARNET,)),
(TOR, (), False, (TOR,)),
(MULTI, (), False, (CLEARNET, TOR)),
(CLEARNET, (), True, (CLEARNET,)),
(TOR, (), True, (TOR,)),
(MULTI, (), True, (TOR,)),
(CLEARNET, (CLEARNET,), False, (CLEARNET,)),
(MULTI, (CLEARNET,), False, (CLEARNET,)),
(MULTI, (CLEARNET,), True, (CLEARNET,)),
(TOR, (TOR,), False, (TOR,)),
(MULTI, (TOR,), False, (TOR,)),
(MULTI, (CLEARNET, TOR), False, (CLEARNET, TOR)),
(MULTI, (CLEARNET, TOR), True, (TOR,)),
)
FAILURES = (
(TOR, (CLEARNET,), False),
(TOR, (CLEARNET,), True),
(CLEARNET, (TOR,), False),
(CLEARNET, (TOR,), True),
)
def test_served_networks(self) -> None:
for node_mode, modes, single, expected in self.CASES:
allowed, parsed_single = allowed_networks(
_cfg(modes=list(modes), single_mode=single), tor_enabled=True
)
with self.subTest(node=node_mode, modes=modes, single=single):
self.assertEqual(
effective_networks(node_mode, allowed, parsed_single), expected
)
def test_mismatch_fails_loudly(self) -> None:
for node_mode, modes, single in self.FAILURES:
allowed, parsed_single = allowed_networks(
_cfg(modes=list(modes), single_mode=single), tor_enabled=True
)
with (
self.subTest(node=node_mode, modes=modes, single=single),
self.assertRaisesRegex(ValueError, "web-app-x"),
):
effective_networks(node_mode, allowed, parsed_single, app="web-app-x")
if __name__ == "__main__":
unittest.main()