feat: enhance network management and lifecycle handling with improved egress setup and validation

This commit is contained in:
2026-09-23 06:23:36 +05:30
parent bd92573158
commit 534a78f2a9
5 changed files with 206 additions and 152 deletions
+75 -39
View File
@@ -5,6 +5,7 @@ import unittest
from copy import deepcopy
from dataclasses import replace
from ipaddress import IPv4Address, IPv4Network
from unittest.mock import patch
from uvm.config import Settings
from uvm.domain import PortRule, VmRecord
@@ -22,6 +23,10 @@ class FakeRunner:
self.links: dict[str, dict] = {}
self.inventory: str | None = None
self.failures: dict[tuple[str, ...], BaseException] = {}
self.default_routes = "default via 192.0.2.1 dev eth0\n"
self.rules: dict[tuple[str, str], list[tuple[str, ...]]] = {
("nat", "POSTROUTING"): [], ("filter", "FORWARD"): [],
}
def run(self, command, *, check=True, capture=False, timeout=None) -> CommandResult:
del capture, timeout
@@ -68,11 +73,21 @@ class FakeRunner:
elif args[:3] == ("ip", "addr", "replace") or args[:2] == ("sysctl", "-w"):
pass
elif args[0] == "iptables":
return CommandResult(args=args, returncode=1 if "-C" in args else 0)
if args[1:4] != ("--wait", "5", "-t"):
raise AssertionError(f"unexpected iptables options: {args}")
table, operation, chain, *rule = args[4:]
rules = self.rules[table, chain]
if operation == "-C":
return CommandResult(args=args, returncode=0 if tuple(rule) in rules else 1)
if operation == "-I":
position, *rule = rule
rules.insert(int(position) - 1, tuple(rule))
else:
raise AssertionError(f"unexpected iptables mutation: {args}")
elif args != ("ip", "route", "show", "default"):
raise AssertionError(f"unexpected host command: {args}")
if args == ("ip", "route", "show", "default"):
return CommandResult(args=args, returncode=0, stdout="default via 192.0.2.1 dev eth0\n")
return CommandResult(args=args, returncode=0, stdout=self.default_routes)
return CommandResult(args=args, returncode=0)
@@ -127,44 +142,61 @@ class NetworkManagerTests(unittest.TestCase):
("ip", "addr", "replace", "10.42.0.1/24", "dev", "uvm0"),
self.runner.calls,
)
self.assertIn(
(
"iptables",
"-A",
"FORWARD",
"-i",
"uvm0",
"-o",
"eth0",
"-s",
"10.42.0.0/24",
"-j",
"ACCEPT",
),
self.runner.calls,
)
checks = [
call
for call in self.runner.calls
if call[:5] == ("iptables", "-t", "nat", "-C", "POSTROUTING")
self.assertIn(("sysctl", "-w", "net.ipv4.ip_forward=1"), self.runner.calls)
nat, = self.runner.rules["nat", "POSTROUTING"]
self.assertEqual(nat[:6], ("-s", "10.42.0.0/24", "!", "-o", "uvm0", "-m"))
self.assertEqual(nat[-2:], ("-j", "MASQUERADE"))
inbound, outbound = self.runner.rules["filter", "FORWARD"]
self.assertEqual(outbound[:4], ("-i", "uvm0", "-s", "10.42.0.0/24"))
self.assertEqual(inbound[:8], (
"-o", "uvm0", "-d", "10.42.0.0/24", "-m", "conntrack",
"--ctstate", "ESTABLISHED,RELATED",
))
self.assertEqual(inbound[-2:], ("-j", "ACCEPT"))
self.assertEqual(outbound[-2:], ("-j", "ACCEPT"))
def test_egress_is_installed_offline_and_survives_uplink_changes_without_extra_rules(self) -> None:
self.runner.default_routes = ""
self.network.ensure_bridge()
offline_rules = deepcopy(self.runner.rules)
self.assertEqual([len(rules) for rules in offline_rules.values()], [1, 2])
for route in (
"default via 192.0.2.1 dev eth0\n",
"default via 198.51.100.1 dev wlan0\n",
"default dev tun0\ndefault via 198.51.100.1 dev wlan0 metric 600\n",
"",
):
with self.subTest(route=route):
self.runner.default_routes = route
self.runner.calls.clear()
self.network.ensure_bridge()
self.assertEqual(self.runner.rules, offline_rules)
self.assertFalse(any("-I" in call for call in self.runner.calls))
self.assertFalse(any("eth0" in rule or "wlan0" in rule or "tun0" in rule
for rules in offline_rules.values() for rule in rules))
def test_egress_exceptions_precede_existing_drops_and_preserve_foreign_and_legacy_rules(self) -> None:
existing = [
("-j", "HOST-FIREWALL"), ("-j", "DROP"),
("-i", "uvm0", "-o", "eth0", "-s", "10.42.0.0/24", "-j", "ACCEPT"),
]
self.assertEqual(len(checks), 1)
self.assertIn(
(
"iptables",
"-t",
"nat",
"-A",
"POSTROUTING",
"-s",
"10.42.0.0/24",
"-o",
"eth0",
"-j",
"MASQUERADE",
),
self.runner.calls,
)
self.runner.rules["filter", "FORWARD"] = list(existing)
self.network.ensure_bridge()
rules = list(self.runner.rules["filter", "FORWARD"])
self.assertEqual(rules[2:], existing)
self.assertTrue(all("uvm-egress" in rule and rule[-1] == "ACCEPT" for rule in rules[:2]))
self.network.ensure_bridge()
self.assertEqual(self.runner.rules["filter", "FORWARD"], rules)
def test_iptables_inspection_errors_are_not_treated_as_missing_rules(self) -> None:
check = ("iptables", "--wait", "5", "-t", "nat", "-C", "POSTROUTING",
"-s", "10.42.0.0/24", "!", "-o", "uvm0",
"-m", "comment", "--comment", "uvm-egress", "-j", "MASQUERADE")
with patch.object(self.runner, "run", return_value=CommandResult(check, 4, stderr="permission denied")) as run:
with self.assertRaisesRegex(CommandError, "could not inspect.*permission denied"):
self.network._ensure_masquerade()
self.assertEqual(run.call_count, 1)
def test_bridge_and_guest_network_support_non_default_prefixes(self) -> None:
settings = Settings(
@@ -178,6 +210,10 @@ class NetworkManagerTests(unittest.TestCase):
("ip", "addr", "replace", "10.50.0.1/16", "dev", "uvm0"),
runner.calls,
)
for rules in runner.rules.values():
for rule in rules:
self.assertIn("10.50.0.0/16", rule)
self.assertNotIn("10.42.0.0/24", rule)
def test_refuses_to_adopt_an_existing_tap_device(self) -> None:
for alias in (None, "foreign-owner", f"uvm:{self.vm.id}"):