From db8906fbabe9fe2e5a0a69ebbf8edb7ba876d64e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Thu, 7 May 2026 11:08:06 +0900 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B0=D1=82=D1=87=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=BE=D1=81=D0=B0=20bridge=20VM=20=D0=B1=D1=8B?= =?UTF-8?q?=D0=BB=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/patch_network_bridge_forwards.py | 148 +++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 scripts/patch_network_bridge_forwards.py diff --git a/scripts/patch_network_bridge_forwards.py b/scripts/patch_network_bridge_forwards.py new file mode 100644 index 0000000..93cf5bc --- /dev/null +++ b/scripts/patch_network_bridge_forwards.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +from pathlib import Path +import sys + +app_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('/opt/virtuality/web/app.py') +core_path = app_path.with_name('network_core.py') +if not core_path.exists(): + raise SystemExit(f'network_core.py not found: {core_path}') + +text = core_path.read_text() +changed = [] + +if 'def route_interface_for_ip(' not in text: + marker = '''def external_interface() -> str: + result = run_cmd(['ip', 'route', 'show', 'default'], timeout=5) + if not result['ok']: + return 'eth0' + match = re.search(r'\\bdev\\s+([^\\s]+)', result['stdout']) + return match.group(1) if match else 'eth0' +''' + helper = marker + ''' + +def route_interface_for_ip(ip: str) -> str: + if not valid_ip(ip): + return NAT_BRIDGE + result = run_cmd(['ip', 'route', 'get', ip], timeout=5) + if not result['ok']: + return NAT_BRIDGE + match = re.search(r'\\bdev\\s+([^\\s]+)', result['stdout']) + return match.group(1) if match else NAT_BRIDGE + + +def forward_guest_interface(item: dict[str, Any]) -> str: + return route_interface_for_ip(str(item.get('guest_ip', ''))) +''' + if marker not in text: + raise SystemExit('external_interface marker not found') + text = text.replace(marker, helper, 1) + changed.append('route_interface_for_ip helper added') +else: + changed.append('route_interface_for_ip helper already present') + +old_render = '''def render_nft_rules() -> str: + ext = external_interface() + lines = [ + 'table ip virtuality {', + ' chain prerouting {', + ' type nat hook prerouting priority dstnat; policy accept;', + ] + for item in load_port_forwards(): + external_ports = nft_port_value(item['external_port_start'], item['external_port_end']) + guest_ports = nft_port_value(item['guest_port_start'], item['guest_port_end']) + lines.append(f" iifname \\"{ext}\\" {item['protocol']} dport {external_ports} dnat to {item['guest_ip']}:{guest_ports}") + lines += [ + ' }', + ' chain postrouting {', + ' type nat hook postrouting priority srcnat; policy accept;', + f' ip saddr {NAT_SUBNET} oifname "{ext}" masquerade', + ' }', + ' chain forward {', + ' type filter hook forward priority filter; policy accept;', + f' ip saddr {NAT_SUBNET} accept', + f' ip daddr {NAT_SUBNET} accept', + ' }', + '}', + ] + return '\\n'.join(lines) + '\\n' +''' +new_render = '''def render_nft_rules() -> str: + ext = external_interface() + forwards = load_port_forwards() + lines = [ + 'table ip virtuality {', + ' chain prerouting {', + ' type nat hook prerouting priority dstnat; policy accept;', + ] + for item in forwards: + external_ports = nft_port_value(item['external_port_start'], item['external_port_end']) + guest_ports = nft_port_value(item['guest_port_start'], item['guest_port_end']) + lines.append(f" iifname \\"{ext}\\" {item['protocol']} dport {external_ports} dnat to {item['guest_ip']}:{guest_ports}") + lines += [ + ' }', + ' chain postrouting {', + ' type nat hook postrouting priority srcnat; policy accept;', + f' ip saddr {NAT_SUBNET} oifname "{ext}" masquerade', + ] + for item in forwards: + guest_iface = forward_guest_interface(item) + lines.append(f' ip daddr {item["guest_ip"]} oifname "{guest_iface}" masquerade') + lines += [ + ' }', + ' chain forward {', + ' type filter hook forward priority filter; policy accept;', + f' ip saddr {NAT_SUBNET} accept', + f' ip daddr {NAT_SUBNET} accept', + ] + for item in forwards: + guest_iface = forward_guest_interface(item) + lines.append(f' iifname "{ext}" oifname "{guest_iface}" ip daddr {item["guest_ip"]} accept') + lines.append(f' iifname "{guest_iface}" oifname "{ext}" ip saddr {item["guest_ip"]} ct state established,related accept') + lines += [ + ' }', + '}', + ] + return '\\n'.join(lines) + '\\n' +''' +if old_render in text: + text = text.replace(old_render, new_render, 1) + changed.append('nft rules now support bridge/static VM interfaces') +elif 'forward_guest_interface(item)' in text and 'ip daddr {item["guest_ip"]}' in text: + changed.append('nft bridge/static rules already present') +else: + raise SystemExit('render_nft_rules marker not found') + +text = text.replace( + "'out', 'on', NAT_BRIDGE,\n 'to', item['guest_ip'],", + "'out', 'on', forward_guest_interface(item),\n 'to', item['guest_ip'],", +) +if "'out', 'on', forward_guest_interface(item)" in text: + changed.append('UFW route rules now use guest route interface') + +old_iptables_line = """ results.append(ensure_iptables_rule(['iptables', '-I', 'FORWARD', '1', '-i', ext, '-o', NAT_BRIDGE, '-p', proto, '-d', guest_ip, '-m', proto, '--dport', guest_port, '-j', 'ACCEPT'])) + results.append(ensure_iptables_rule(['iptables', '-I', 'FORWARD', '1', '-i', NAT_BRIDGE, '-o', ext, '-s', guest_ip, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'])) + results.append(ensure_iptables_rule(['iptables', '-t', 'nat', '-I', 'PREROUTING', '1', '-i', ext, '-p', proto, '-m', proto, '--dport', external_port, '-j', 'DNAT', '--to-destination', guest_to])) +""" +new_iptables_line = """ guest_iface = forward_guest_interface(item) + results.append(ensure_iptables_rule(['iptables', '-I', 'FORWARD', '1', '-i', ext, '-o', guest_iface, '-p', proto, '-d', guest_ip, '-m', proto, '--dport', guest_port, '-j', 'ACCEPT'])) + results.append(ensure_iptables_rule(['iptables', '-I', 'FORWARD', '1', '-i', guest_iface, '-o', ext, '-s', guest_ip, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'])) + results.append(ensure_iptables_rule(['iptables', '-t', 'nat', '-I', 'PREROUTING', '1', '-i', ext, '-p', proto, '-m', proto, '--dport', external_port, '-j', 'DNAT', '--to-destination', guest_to])) + results.append(ensure_iptables_rule(['iptables', '-t', 'nat', '-I', 'POSTROUTING', '1', '-d', guest_ip, '-o', guest_iface, '-j', 'MASQUERADE'])) +""" +if old_iptables_line in text: + text = text.replace(old_iptables_line, new_iptables_line, 1) + changed.append('iptables fallback now supports bridge/static VM interfaces') +elif "guest_iface = forward_guest_interface(item)" in text and "'-o', guest_iface" in text: + changed.append('iptables bridge/static fallback already present') +else: + raise SystemExit('iptables fallback marker not found') + +text = text.replace( + "'watch_internal': f\"sudo tcpdump -ni {NAT_BRIDGE} 'host {vm_ip or ''} and {protocol} port {int(guest_port)}'\",", + "'watch_internal': f\"sudo tcpdump -ni {route_interface_for_ip(vm_ip) if vm_ip else NAT_BRIDGE} 'host {vm_ip or ''} and {protocol} port {int(guest_port)}'\",", +) + +core_path.write_text(text) +print('network bridge forward patch applied:') +for item in changed: + print(f'- {item}')