Страница сети была переведена на единое меню

Этот коммит содержится в:
Виктор
2026-05-07 10:25:04 +09:00
родитель d529c5672a
Коммит 4779b74464
+238 -251
Просмотреть файл
@@ -7,274 +7,261 @@
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css">
</head> </head>
<body> <body>
<div class="shell"> <div class="v-layout">
<header class="topbar"> {% include "_sidebar.html" %}
<div>
<div class="brand">Сеть</div>
<div class="muted">NAT Router для VPS, проброс одиночных портов и диапазонов к VM</div>
</div>
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill accent" href="/vm/create">+ Создать VM</a>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
</div>
</header>
{% if error %} <main class="v-main">
<div class="alert danger">{{ error }}</div> <header class="v-topbar">
{% endif %} <div>
<div class="v-title">Сеть</div>
<section class="grid stats"> <div class="v-subtitle">NAT Router для VPS, проброс одиночных портов и диапазонов к VM</div>
<article class="card stat">
<div class="label">NAT network</div>
<div class="value small">{{ ctx.nat.name }}</div>
<span class="status {{ 'ok' if ctx.nat.exists else 'warn' }}">{{ 'ready' if ctx.nat.exists else 'not created' }}</span>
</article>
<article class="card stat">
<div class="label">Subnet</div>
<div class="value small">{{ ctx.nat.subnet }}</div>
</article>
<article class="card stat">
<div class="label">External interface</div>
<div class="value small">{{ ctx.external_interface }}</div>
</article>
<article class="card stat">
<div class="label">IPv4 forward</div>
<div class="value small">{{ ctx.ip_forward }}</div>
</article>
</section>
<section class="grid two">
<article class="card">
<div class="card-head">
<h2>Virtuality NAT Router</h2>
<span class="pill">{{ ctx.nat.bridge }}</span>
</div> </div>
<div class="meta-grid"> <div class="top-meta">
<div><span>Gateway</span><strong>{{ ctx.nat.gateway }}</strong></div> <span>user: {{ user }}</span>
<div><span>DHCP</span><strong>{{ ctx.nat.dhcp }}</strong></div> <a class="link-pill" href="/">← Дашборд</a>
<div><span>Bridge</span><strong>{{ ctx.nat.bridge }}</strong></div> <form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
</div> </div>
<form method="post" action="/network/nat/setup" class="inline-form top-space"> </header>
<button class="primary">Создать / починить NAT-сеть</button>
</form>
<pre>{{ ctx.nat.info }}</pre>
</article>
<article class="card"> {% if error %}
<div class="card-head"> <div class="alert danger">{{ error }}</div>
<h2>DHCP leases</h2> {% endif %}
<span class="pill">virsh net-dhcp-leases</span>
</div>
<pre>{{ ctx.nat.leases }}</pre>
<div class="notice top-space">IP для проброса определяется автоматически: сначала через qemu-guest-agent, затем по DHCP leases и MAC-адресу VM.</div>
</article>
</section>
<section class="grid two"> <section class="grid stats">
<article class="card"> <article class="card stat">
<div class="card-head"> <div class="label">NAT network</div>
<h2>Добавить проброс порта</h2> <div class="value small">{{ ctx.nat.name }}</div>
<span class="pill">DNAT auto IP · ranges</span> <span class="status {{ 'ok' if ctx.nat.exists else 'warn' }}">{{ 'ready' if ctx.nat.exists else 'not created' }}</span>
</div> </article>
<form method="post" action="/network/forward/add" class="form-grid"> <article class="card stat">
<label> <div class="label">Subnet</div>
<span>Описание</span> <div class="value small">{{ ctx.nat.subnet }}</div>
<input type="text" name="note" placeholder="Например: сайт, SSH, SIP, RTP MikoPBX"> </article>
</label> <article class="card stat">
<div class="label">External interface</div>
<div class="value small">{{ ctx.external_interface }}</div>
</article>
<article class="card stat">
<div class="label">IPv4 forward</div>
<div class="value small">{{ ctx.ip_forward }}</div>
</article>
</section>
<label> <section class="grid two">
<span>Внешний порт или диапазон</span> <article class="card">
<input type="text" name="external_port" pattern="[0-9]{1,5}([:-][0-9]{1,5})?" placeholder="80, 443, 2222 или 10000-20000" required> <div class="card-head">
</label> <h2>Virtuality NAT Router</h2>
<span class="pill">{{ ctx.nat.bridge }}</span>
<label>
<span>Машина</span>
<select name="vm_name" required>
{% for vm in vms %}
<option value="{{ vm.name }}">{{ vm.name }} — {{ vm.state }}</option>
{% endfor %}
</select>
</label>
<input type="hidden" name="guest_ip" value="auto">
<label>
<span>Внутренний порт или диапазон</span>
<input type="text" name="guest_port" list="standard-ports" pattern="[0-9]{1,5}([:-][0-9]{1,5})?" placeholder="Выбери стандартный или введи 10000-20000" required>
<datalist id="standard-ports">
<option value="20" label="FTP data"></option>
<option value="21" label="FTP"></option>
<option value="22" label="SSH"></option>
<option value="25" label="SMTP"></option>
<option value="53" label="DNS"></option>
<option value="80" label="HTTP / web"></option>
<option value="110" label="POP3"></option>
<option value="123" label="NTP"></option>
<option value="143" label="IMAP"></option>
<option value="443" label="HTTPS / SSL web"></option>
<option value="465" label="SMTPS"></option>
<option value="587" label="SMTP submission"></option>
<option value="993" label="IMAPS"></option>
<option value="995" label="POP3S"></option>
<option value="1194" label="OpenVPN"></option>
<option value="1433" label="Microsoft SQL Server"></option>
<option value="1521" label="Oracle Database"></option>
<option value="2049" label="NFS"></option>
<option value="3306" label="MySQL / MariaDB"></option>
<option value="3389" label="RDP / Windows Remote Desktop"></option>
<option value="5060" label="SIP"></option>
<option value="5061" label="SIP TLS"></option>
<option value="5432" label="PostgreSQL"></option>
<option value="5900" label="VNC"></option>
<option value="6379" label="Redis"></option>
<option value="8000" label="HTTP dev"></option>
<option value="8080" label="HTTP alternative"></option>
<option value="8443" label="HTTPS alternative"></option>
<option value="10000" label="Webmin / RTP start"></option>
<option value="10000-20000" label="RTP / VoIP media range"></option>
</datalist>
</label>
<label>
<span>Протокол</span>
<select name="protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</label>
<div class="notice">
Можно указать одиночный порт или диапазон. Для VoIP/RTP обычно: внешний 10000-20000 → внутренний 10000-20000, протокол udp. Длины диапазонов должны совпадать.
</div> </div>
<div class="meta-grid">
<div><span>Gateway</span><strong>{{ ctx.nat.gateway }}</strong></div>
<div><span>DHCP</span><strong>{{ ctx.nat.dhcp }}</strong></div>
<div><span>Bridge</span><strong>{{ ctx.nat.bridge }}</strong></div>
</div>
<form method="post" action="/network/nat/setup" class="inline-form top-space">
<button class="primary">Создать / починить NAT-сеть</button>
</form>
<pre>{{ ctx.nat.info }}</pre>
</article>
<button class="primary wide">Добавить и применить</button> <article class="card">
</form> <div class="card-head">
</article> <h2>DHCP leases</h2>
<span class="pill">virsh net-dhcp-leases</span>
</div>
<pre>{{ ctx.nat.leases }}</pre>
<div class="notice top-space">IP для проброса определяется автоматически: сначала через qemu-guest-agent, затем по DHCP leases и MAC-адресу VM.</div>
</article>
</section>
<article class="card"> <section class="grid two">
<div class="card-head"> <article class="card">
<h2>Диагностика публичного доступа</h2> <div class="card-head">
<span class="pill">VM → Internet</span> <h2>Добавить проброс порта</h2>
</div> <span class="pill">DNAT auto IP · ranges</span>
<form method="post" action="/network/diagnose" class="form-grid"> </div>
<label> <form method="post" action="/network/forward/add" class="form-grid">
<span>Виртуальная машина</span>
<select name="vm_name" required>
{% for vm in vms %}
<option value="{{ vm.name }}">{{ vm.name }} — {{ vm.state }}</option>
{% endfor %}
</select>
</label>
<div class="form-row">
<label> <label>
<span>Внешний порт</span> <span>Описание</span>
<input type="number" name="external_port" min="1" max="65535" value="80" required> <input type="text" name="note" placeholder="Например: сайт, SSH, SIP, RTP MikoPBX">
</label> </label>
<label> <label>
<span>Порт внутри VM</span> <span>Внешний порт или диапазон</span>
<input type="number" name="guest_port" min="1" max="65535" value="80" required> <input type="text" name="external_port" pattern="[0-9]{1,5}([:-][0-9]{1,5})?" placeholder="80, 443, 2222 или 10000-20000" required>
</label> </label>
<label>
<span>Машина</span>
<select name="vm_name" required>
{% for vm in vms %}
<option value="{{ vm.name }}">{{ vm.name }} — {{ vm.state }}</option>
{% endfor %}
</select>
</label>
<input type="hidden" name="guest_ip" value="auto">
<label>
<span>Внутренний порт или диапазон</span>
<input type="text" name="guest_port" list="standard-ports" pattern="[0-9]{1,5}([:-][0-9]{1,5})?" placeholder="Выбери стандартный или введи 10000-20000" required>
<datalist id="standard-ports">
<option value="20" label="FTP data"></option>
<option value="21" label="FTP"></option>
<option value="22" label="SSH"></option>
<option value="25" label="SMTP"></option>
<option value="53" label="DNS"></option>
<option value="80" label="HTTP / web"></option>
<option value="110" label="POP3"></option>
<option value="123" label="NTP"></option>
<option value="143" label="IMAP"></option>
<option value="443" label="HTTPS / SSL web"></option>
<option value="465" label="SMTPS"></option>
<option value="587" label="SMTP submission"></option>
<option value="993" label="IMAPS"></option>
<option value="995" label="POP3S"></option>
<option value="1194" label="OpenVPN"></option>
<option value="1433" label="Microsoft SQL Server"></option>
<option value="1521" label="Oracle Database"></option>
<option value="2049" label="NFS"></option>
<option value="3306" label="MySQL / MariaDB"></option>
<option value="3389" label="RDP / Windows Remote Desktop"></option>
<option value="5060" label="SIP"></option>
<option value="5061" label="SIP TLS"></option>
<option value="5432" label="PostgreSQL"></option>
<option value="5900" label="VNC"></option>
<option value="6379" label="Redis"></option>
<option value="8000" label="HTTP dev"></option>
<option value="8080" label="HTTP alternative"></option>
<option value="8443" label="HTTPS alternative"></option>
<option value="10000" label="Webmin / RTP start"></option>
<option value="10000-20000" label="RTP / VoIP media range"></option>
</datalist>
</label>
<label>
<span>Протокол</span>
<select name="protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</label>
<div class="notice">Можно указать одиночный порт или диапазон. Для VoIP/RTP обычно: внешний 10000-20000 → внутренний 10000-20000, протокол udp. Длины диапазонов должны совпадать.</div>
<button class="primary wide">Добавить и применить</button>
</form>
</article>
<article class="card">
<div class="card-head">
<h2>Диагностика публичного доступа</h2>
<span class="pill">VM → Internet</span>
</div> </div>
<label> <form method="post" action="/network/diagnose" class="form-grid">
<span>Протокол</span> <label>
<select name="protocol"> <span>Виртуальная машина</span>
<option value="tcp">tcp</option> <select name="vm_name" required>
<option value="udp">udp</option> {% for vm in vms %}
</select> <option value="{{ vm.name }}">{{ vm.name }} — {{ vm.state }}</option>
</label> {% endfor %}
<button class="primary wide">Проверить доступ</button> </select>
</form> </label>
</article> <div class="form-row">
</section> <label>
<span>Внешний порт</span>
{% if diagnostics %} <input type="number" name="external_port" min="1" max="65535" value="80" required>
<section class="card"> </label>
<div class="card-head"> <label>
<h2>Результат диагностики</h2> <span>Порт внутри VM</span>
<span class="pill">{{ diagnostics.vm_name }} · {{ diagnostics.protocol }} {{ diagnostics.external_port }} → {{ diagnostics.guest_port }}</span> <input type="number" name="guest_port" min="1" max="65535" value="80" required>
</div> </label>
<div class="notice">{{ diagnostics.verdict }}</div> </div>
<div class="meta-grid top-space"> <label>
<div><span>VM IP</span><strong>{{ diagnostics.vm_ip or 'not found' }}</strong></div> <span>Протокол</span>
<div><span>External interface</span><strong>{{ diagnostics.external_interface }}</strong></div> <select name="protocol">
<div><span>Mapping</span><strong>{{ diagnostics.external_port }} → {{ diagnostics.guest_port }}</strong></div> <option value="tcp">tcp</option>
</div> <option value="udp">udp</option>
<table class="top-space"> </select>
<thead><tr><th>Проверка</th><th>Статус</th><th>Детали</th></tr></thead> </label>
<tbody> <button class="primary wide">Проверить доступ</button>
{% for check in diagnostics.checks %} </form>
<tr>
<td class="strong">{{ check.name }}</td>
<td><span class="status {{ 'ok' if check.ok else 'err' }}">{{ 'OK' if check.ok else 'FAIL' }}</span></td>
<td class="muted">{{ check.detail }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="grid two top-space">
<article>
<div class="label">Проверить входящий трафик</div>
<pre>{{ diagnostics.commands.watch_external }}</pre>
</article> </article>
<article> </section>
<div class="label">Проверить трафик к VM</div>
<pre>{{ diagnostics.commands.watch_internal }}</pre>
</article>
</div>
</section>
{% endif %}
<section class="card"> {% if diagnostics %}
<div class="card-head"> <section class="card">
<h2>Пробросы портов</h2> <div class="card-head">
<form method="post" action="/network/apply"><button>Применить правила</button></form> <h2>Результат диагностики</h2>
</div> <span class="pill">{{ diagnostics.vm_name }} · {{ diagnostics.protocol }} {{ diagnostics.external_port }} → {{ diagnostics.guest_port }}</span>
<table> </div>
<thead> <div class="notice">{{ diagnostics.verdict }}</div>
<tr><th>Проброс</th><th>VM</th><th>Внутренний адрес</th><th>Протокол</th><th>Описание</th><th>Действия</th></tr> <div class="meta-grid top-space">
</thead> <div><span>VM IP</span><strong>{{ diagnostics.vm_ip or 'not found' }}</strong></div>
<tbody> <div><span>External interface</span><strong>{{ diagnostics.external_interface }}</strong></div>
{% for item in ctx.forwards %} <div><span>Mapping</span><strong>{{ diagnostics.external_port }} → {{ diagnostics.guest_port }}</strong></div>
<tr> </div>
<td class="strong"> <table class="top-space">
{{ item.external_port_label|default(item.external_port) }} → {{ item.guest_port_label|default(item.guest_port) }} <thead><tr><th>Проверка</th><th>Статус</th><th>Детали</th></tr></thead>
{% if item.is_range %}<span class="status warn">range</span>{% endif %} <tbody>
</td> {% for check in diagnostics.checks %}
<td>{{ item.vm_name }}</td> <tr>
<td>{{ item.guest_ip }}</td> <td class="strong">{{ check.name }}</td>
<td><span class="status ok">{{ item.protocol }}</span></td> <td><span class="status {{ 'ok' if check.ok else 'err' }}">{{ 'OK' if check.ok else 'FAIL' }}</span></td>
<td>{{ item.note }}</td> <td class="muted">{{ check.detail }}</td>
<td> </tr>
<div class="actions"> {% endfor %}
<form method="post" action="/network/diagnose"> </tbody>
<input type="hidden" name="vm_name" value="{{ item.vm_name }}"> </table>
<input type="hidden" name="external_port" value="{{ item.external_port }}"> <div class="grid two top-space">
<input type="hidden" name="guest_port" value="{{ item.guest_port }}"> <article><div class="label">Проверить входящий трафик</div><pre>{{ diagnostics.commands.watch_external }}</pre></article>
<input type="hidden" name="protocol" value="{{ item.protocol }}"> <article><div class="label">Проверить трафик к VM</div><pre>{{ diagnostics.commands.watch_internal }}</pre></article>
<button>Диагностика</button> </div>
</form> </section>
<form method="post" action="/network/forward/{{ item.id }}/delete"> {% endif %}
<button class="danger">Удалить</button>
</form>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">Пробросов пока нет</td></tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card"> <section class="card">
<div class="card-head"> <div class="card-head">
<h2>nftables preview</h2> <h2>Пробросы портов</h2>
<span class="pill">/etc/virtuality/nftables/virtuality.nft</span> <form method="post" action="/network/apply"><button>Применить правила</button></form>
</div> </div>
<pre>{{ ctx.nft_rules }}</pre> <table>
</section> <thead>
<tr><th>Проброс</th><th>VM</th><th>Внутренний адрес</th><th>Протокол</th><th>Описание</th><th>Действия</th></tr>
</thead>
<tbody>
{% for item in ctx.forwards %}
<tr>
<td class="strong">
{{ item.external_port_label|default(item.external_port) }} → {{ item.guest_port_label|default(item.guest_port) }}
{% if item.is_range %}<span class="status warn">range</span>{% endif %}
</td>
<td>{{ item.vm_name }}</td>
<td>{{ item.guest_ip }}</td>
<td><span class="status ok">{{ item.protocol }}</span></td>
<td>{{ item.note }}</td>
<td>
<div class="actions">
<form method="post" action="/network/diagnose">
<input type="hidden" name="vm_name" value="{{ item.vm_name }}">
<input type="hidden" name="external_port" value="{{ item.external_port }}">
<input type="hidden" name="guest_port" value="{{ item.guest_port }}">
<input type="hidden" name="protocol" value="{{ item.protocol }}">
<button>Диагностика</button>
</form>
<form method="post" action="/network/forward/{{ item.id }}/delete"><button class="danger">Удалить</button></form>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">Пробросов пока нет</td></tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<div class="card-head">
<h2>nftables preview</h2>
<span class="pill">/etc/virtuality/nftables/virtuality.nft</span>
</div>
<pre>{{ ctx.nft_rules }}</pre>
</section>
</main>
</div> </div>
<script src="/static/panel.js" defer></script>
</body> </body>
</html> </html>