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
+29 -102
View File
@@ -9,7 +9,7 @@ from ipaddress import IPv4Address
from .config import Settings
from .domain import VmRecord
from .errors import ConflictError, UvmError, ValidationError
from .errors import CommandError, ConflictError, UvmError, ValidationError
from .firewall import FirewallManager
from .system import CommandRunner
@@ -178,111 +178,38 @@ class NetworkManager:
raise UvmError(f"could not delete TAP device {name}{suffix}")
def _ensure_masquerade(self) -> None:
route = self._runner.run(("ip", "route", "show", "default"), capture=True)
match = re.search(r"\bdev\s+(\S+)", route.stdout)
if match is None:
return
uplink = match.group(1)
rule = (
"iptables",
"-t",
"nat",
"-C",
"POSTROUTING",
"-s",
str(self._settings.network),
"-o",
uplink,
"-j",
"MASQUERADE",
)
present = self._runner.run(rule, check=False)
if present.returncode != 0:
self._runner.run(
(
"iptables",
"-t",
"nat",
"-A",
"POSTROUTING",
"-s",
str(self._settings.network),
"-o",
uplink,
"-j",
"MASQUERADE",
)
)
# Match the guest network, not a snapshot of the host's default uplink.
# Routing selects the current interface for each destination, even after
# an offline boot, a reconnect, or a switch between Wi-Fi/Ethernet/VPN.
bridge = self._settings.bridge
network = str(self._settings.network)
owner = ("-m", "comment", "--comment", "uvm-egress")
self._ensure_iptables_rule(
(
"iptables",
"-C",
"FORWARD",
"-i",
self._settings.bridge,
"-o",
uplink,
"-s",
str(self._settings.network),
"-j",
"ACCEPT",
),
(
"iptables",
"-A",
"FORWARD",
"-i",
self._settings.bridge,
"-o",
uplink,
"-s",
str(self._settings.network),
"-j",
"ACCEPT",
),
"nat", "POSTROUTING",
("-s", network, "!", "-o", bridge, *owner, "-j", "MASQUERADE"),
)
# Native bridge policy still checks guest identity and private ports.
# This also permits peer traffic when br_netfilter is already enabled.
self._ensure_iptables_rule(
"filter", "FORWARD",
("-i", bridge, "-s", network, *owner, "-j", "ACCEPT"),
)
self._ensure_iptables_rule(
(
"iptables",
"-C",
"FORWARD",
"-i",
uplink,
"-o",
self._settings.bridge,
"-d",
str(self._settings.network),
"-m",
"conntrack",
"--ctstate",
"ESTABLISHED,RELATED",
"-j",
"ACCEPT",
),
(
"iptables",
"-A",
"FORWARD",
"-i",
uplink,
"-o",
self._settings.bridge,
"-d",
str(self._settings.network),
"-m",
"conntrack",
"--ctstate",
"ESTABLISHED,RELATED",
"-j",
"ACCEPT",
),
"filter", "FORWARD",
("-o", bridge, "-d", network, "-m", "conntrack",
"--ctstate", "ESTABLISHED,RELATED", *owner, "-j", "ACCEPT"),
)
def _ensure_iptables_rule(
self,
check_rule: tuple[str, ...],
add_rule: tuple[str, ...],
self, table: str, chain: str, rule: tuple[str, ...],
) -> None:
present = self._runner.run(check_rule, check=False)
if present.returncode != 0:
self._runner.run(add_rule)
command = ("iptables", "--wait", "5", "-t", table)
present = self._runner.run((*command, "-C", chain, *rule), check=False, capture=True)
if present.returncode == 0:
return
if present.returncode != 1:
detail = present.stderr.strip() or present.stdout.strip()
raise CommandError(f"could not inspect UVM {table}/{chain} rule ({present.returncode}): {detail}")
# Scoped exceptions must precede terminal drops in the host's chains.
# Keep unrelated rules and the global forwarding policy intact.
self._runner.run((*command, "-I", chain, "1", *rule), capture=True)