Patch-скрипты были слиты в исходники web-панели

Все 21 патч из install_web_panel.sh применены к web/app.py,
web/network_core.py и web/static/app.css, механизм текстовых патчей
удалён из установщиков, а сами patch_*.py (включая 7 неиспользуемых
устаревших) удалены из scripts/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKUoZwHMCHnytfswFVRUHw
Этот коммит содержится в:
Claude
2026-08-15 19:01:43 +00:00
родитель 423c00b96a
Коммит 77a263ec86
33 изменённых файлов: 1379 добавлений и 4242 удалений
Разница между файлами не показана из-за своего большого размера Загрузить разницу
+66 -12
Просмотреть файл
@@ -72,6 +72,18 @@ def iptables_port_value(start: int, end: int) -> str:
return str(int(start)) if int(start) == int(end) else f'{int(start)}:{int(end)}'
def iptables_dnat_port_value(start: int, end: int) -> str:
return str(int(start)) if int(start) == int(end) else f'{int(start)}-{int(end)}'
def successful_results(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [item for item in results if item.get('ok')]
def failed_results(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [item for item in results if not item.get('ok')]
def port_label(start: int, end: int) -> str:
return str(int(start)) if int(start) == int(end) else f'{int(start)}-{int(end)}'
@@ -128,6 +140,20 @@ def external_interface() -> str:
return match.group(1) if match else 'eth0'
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', '')))
def ip_forward_state() -> str:
path = Path('/proc/sys/net/ipv4/ip_forward')
if not path.exists():
@@ -352,12 +378,13 @@ def delete_port_forward(forward_id: str) -> None:
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 load_port_forwards():
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}")
@@ -366,11 +393,22 @@ def render_nft_rules() -> str:
' 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 += [
' }',
'}',
]
@@ -392,7 +430,7 @@ def apply_ufw_route_rules(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
cmd = [
'ufw', 'route', 'allow',
'in', 'on', ext,
'out', 'on', NAT_BRIDGE,
'out', 'on', forward_guest_interface(item),
'to', item['guest_ip'],
'port', guest_port,
'proto', item['protocol'],
@@ -429,10 +467,13 @@ def apply_iptables_fallback(items: list[dict[str, Any]]) -> list[dict[str, Any]]
guest_ip = item['guest_ip']
external_port = iptables_port_value(item['external_port_start'], item['external_port_end'])
guest_port = iptables_port_value(item['guest_port_start'], item['guest_port_end'])
guest_to = f"{guest_ip}:{iptables_port_value(item['guest_port_start'], item['guest_port_end'])}"
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']))
guest_to_port = iptables_dnat_port_value(item['guest_port_start'], item['guest_port_end'])
guest_to = f"{guest_ip}:{guest_to_port}"
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']))
results.append(ensure_iptables_rule(['iptables', '-t', 'nat', '-I', 'POSTROUTING', '1', '-s', NAT_SUBNET, '-o', ext, '-j', 'MASQUERADE']))
return results
@@ -443,16 +484,29 @@ def apply_port_forwards() -> dict[str, Any]:
disable_rp_filter()
items = load_port_forwards()
NFT_FILE.write_text(render_nft_rules())
run_cmd(['nft', 'delete', 'table', 'ip', 'virtuality'], timeout=8)
result = run_cmd(['nft', '-f', str(NFT_FILE)], timeout=15)
if not result['ok']:
raise NetworkError(result['stderr'] or result['stdout'] or 'Не удалось применить nftables-правила')
nft_delete = run_cmd(['nft', 'delete', 'table', 'ip', 'virtuality'], timeout=8)
nft_apply = run_cmd(['nft', '-f', str(NFT_FILE)], timeout=15)
ufw_results = apply_ufw_route_rules(items)
iptables_results = apply_iptables_fallback(items)
iptables_ok = bool(successful_results(iptables_results)) or not items
if not nft_apply['ok'] and not iptables_ok:
details = [
nft_apply.get('stderr') or nft_apply.get('stdout') or 'nftables не применился',
*[
item.get('stderr') or item.get('stdout') or item.get('cmd', 'iptables rule failed')
for item in failed_results(iptables_results)
],
]
raise NetworkError('Не удалось применить правила проброса: ' + ' | '.join([d for d in details if d]))
return {
'ok': True,
'ok': nft_apply['ok'] or iptables_ok,
'file': str(NFT_FILE),
'rules': render_nft_rules(),
'nft_delete': nft_delete,
'nft_apply': nft_apply,
'ufw': ufw_results,
'iptables': iptables_results,
}
@@ -505,7 +559,7 @@ def diagnose_public_access(vm_name: str, external_port: int, guest_port: int, pr
nft_has_range_rule = bool(matching_forward and vm_ip and matching_forward['external_port_label'] in nft_text and f"{vm_ip}:{matching_forward['guest_port_label']}" in nft_text)
nft_has_rule = nft_has_single_rule or nft_has_range_rule
iptables_has_prerouting = bool(vm_ip and f'--dport {int(external_port)} -j DNAT --to-destination {vm_ip}:{int(guest_port)}' in ipt_nat_text)
iptables_has_range_prerouting = bool(matching_forward and vm_ip and matching_forward['external_port_label'].replace('-', ':') in ipt_nat_text and f"{vm_ip}:{matching_forward['guest_port_label'].replace('-', ':')}" in ipt_nat_text)
iptables_has_range_prerouting = bool(matching_forward and vm_ip and matching_forward['external_port_label'].replace('-', ':') in ipt_nat_text and (f"{vm_ip}:{matching_forward['guest_port_label']}" in ipt_nat_text or f"{vm_ip}:{matching_forward['guest_port_label'].replace('-', ':')}" in ipt_nat_text))
iptables_has_prerouting = iptables_has_prerouting or iptables_has_range_prerouting
iptables_has_forward = bool(vm_ip and f'-d {vm_ip}/32' in ipt_forward_text and (f'--dport {int(guest_port)}' in ipt_forward_text or (matching_forward and matching_forward['guest_port_label'].replace('-', ':') in ipt_forward_text)))
ufw_has_route = bool(vm_ip and vm_ip in ufw_text and (str(int(guest_port)) in ufw_text or (matching_forward and matching_forward['guest_port_label'].replace('-', ':') in ufw_text)))
@@ -545,7 +599,7 @@ def diagnose_public_access(vm_name: str, external_port: int, guest_port: int, pr
'checks': checks,
'commands': {
'watch_external': f"sudo tcpdump -ni {ext} '{protocol} port {int(external_port)}'",
'watch_internal': f"sudo tcpdump -ni {NAT_BRIDGE} 'host {vm_ip or '<VM_IP>'} 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 '<VM_IP>'} and {protocol} port {int(guest_port)}'",
},
'raw': {
'domifaddr': domifaddr['stdout'] or domifaddr['stderr'],
+23
Просмотреть файл
@@ -295,3 +295,26 @@ body.upload-active .v-nav a, body.upload-active .top-meta a, body.upload-active
table { display: block; overflow-x: auto; border-spacing: 0 8px; }
.icon-action em { display: none; }
}
.resource-mini-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; margin-bottom: 10px; }
.resource-mini-grid div { display: grid; gap: 3px; padding: 8px; border: 1px solid var(--line); border-radius: 7px; background: var(--panel-2); }
.resource-mini-grid span { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
.resource-mini-grid b { font-size: 14px; overflow-wrap: anywhere; }
.boot-order-list { display: grid; gap: 8px; }
.boot-order-item { display: flex; align-items: center; gap: 10px; padding: 9px; border: 1px solid var(--line); border-radius: 7px; background: var(--panel-2); cursor: grab; }
.boot-order-item:active { cursor: grabbing; }
.boot-order-item.dragging { opacity: .55; border-color: var(--accent); }
.boot-order-item small { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; }
.drag-handle { color: var(--muted); font-weight: 900; }
@media (max-width: 1180px) { .three { grid-template-columns: 1fr; } .resource-mini-grid { grid-template-columns: 1fr; } }
/* Virtuality live status + toast layer */
#v-toast-wrap { position: fixed; top: 14px; right: 14px; z-index: 10000; display: grid; gap: 8px; width: min(360px, calc(100vw - 28px)); pointer-events: none; }
.v-toast { transform: translateY(-8px); opacity: 0; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--line); background: #fff; color: var(--text); box-shadow: 0 12px 32px rgba(15,23,42,.18); font-size: 13px; font-weight: 800; transition: .22s ease; }
.v-toast.show { transform: translateY(0); opacity: 1; }
.v-toast.ok { border-color: rgba(22,163,74,.28); box-shadow: 0 12px 32px rgba(22,163,74,.16); }
.v-toast.warn { border-color: rgba(217,119,6,.32); box-shadow: 0 12px 32px rgba(217,119,6,.16); }
.v-toast.err { border-color: rgba(220,38,38,.32); box-shadow: 0 12px 32px rgba(220,38,38,.16); }
.live-badge { margin-left: 10px; vertical-align: middle; }