200e078bc5
- Add daemon/ module with aiohttp server, sync client, and handler registry - Add daemon/handlers/ for privileged operations (acme, dnsmasq, firewall, logs, nginx, wireguard) - Add system/acme-deploy.py, vacuum-walld sudoers and systemd service - Update API routes to use daemon client instead of lib/ directly - Update lib/, tests/, and webui/ for new architecture - Update docs and deployment scripts
132 lines
5.7 KiB
HTML
132 lines
5.7 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}NAT - Vacuum Wall{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>NAT & Port Forwarding</h1>
|
|
<div class="subtitle">Masquerading and destination NAT rules</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-title">Masquerade (Source NAT)</div>
|
|
|
|
<div class="card">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Zone</th>
|
|
<th style="width:120px;">Masquerade</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for zone in (zones or []) %}
|
|
<tr>
|
|
<td><strong>{{ zone.get('name', 'unnamed') }}</strong></td>
|
|
<td>
|
|
<form hx-post="/api/firewall/masquerade" hx-encoding="json" hx-vals='{"zone": "{{ zone.get('name', '') }}", "enable": JSON.stringify(this.checked)}' hx-swap="none" hx-on::after-request="if(evt.detail.successful){ showSuccessToast('Masquerade '+(this.checked?'enabled':'disabled')+' for {{ zone.get('name', '') }}') } else { this.checked=!this.checked; }">
|
|
<label class="switch">
|
|
<input type="checkbox"
|
|
{% if zone.get('masquerade') %}checked{% endif %}
|
|
id="masq-{{ zone.get('name', '') }}"
|
|
hx-trigger="change from:#masq-{{ zone.get('name', '') }}"
|
|
disabled>
|
|
<span class="slider"></span>
|
|
</label>
|
|
<button type="submit" style="display:none"></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if not (zones or []) %}
|
|
<tr>
|
|
<td colspan="2" class="text-muted text-sm">No zones configured</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section-title">Port Forwarding (DNAT)</div>
|
|
|
|
<div class="card mb-4">
|
|
<h3>Add Forward Rule</h3>
|
|
<form hx-post="/api/firewall/forward-port" hx-encoding="json" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/firewall/config', document.getElementById('forward-rows'), renderForwardsFromConfig); showSuccessToast('Forward rule added'); }">
|
|
<div class="inline-form">
|
|
<div class="form-group">
|
|
<label for="fw-zone">Zone</label>
|
|
<select id="fw-zone" name="zone" required>
|
|
<option value="">— Select —</option>
|
|
{% for zone in (zones or []) %}
|
|
<option value="{{ zone.get('name', '') }}">{{ zone.get('name', '') }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="fw-protocol">Protocol</label>
|
|
<select id="fw-protocol" name="proto">
|
|
<option value="tcp">TCP</option>
|
|
<option value="udp">UDP</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="fw-port">Port</label>
|
|
<input type="number" id="fw-port" name="port" placeholder="80" min="1" max="65535" required style="width:80px;">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="fw-target">Target Address</label>
|
|
<input type="text" id="fw-target" name="toaddr" placeholder="192.168.1.100" required style="width:160px;">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="fw-target-port">Target Port</label>
|
|
<input type="number" id="fw-target-port" name="toport" placeholder="80" min="1" max="65535" style="width:90px;">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Add</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Zone</th>
|
|
<th>Proto</th>
|
|
<th>Port</th>
|
|
<th>Target</th>
|
|
<th>Tgt Port</th>
|
|
<th style="width:80px;">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="forward-rows">
|
|
{% set all_forwards = [] %}
|
|
{% for zone in (zones or []) %}
|
|
{% for fwd in zone.get('forward_ports', []) %}
|
|
{% set _ = all_forwards.append({'zone': zone.get('name'), 'proto': fwd.get('proto'), 'port': fwd.get('port'), 'toaddr': fwd.get('toaddr'), 'toport': fwd.get('toport')}) %}
|
|
{% endfor %}
|
|
{% endfor %}
|
|
{% for fwd in all_forwards %}
|
|
<tr>
|
|
<td><strong>{{ fwd.zone }}</strong></td>
|
|
<td><span class="badge badge-info">{{ fwd.proto }}</span></td>
|
|
<td>{{ fwd.port }}</td>
|
|
<td>{{ fwd.toaddr }}</td>
|
|
<td>{{ fwd.toport }}</td>
|
|
<td>
|
|
<form hx-delete="/api/firewall/forward-port/{{ fwd.zone }}/{{ fwd.port }}/{{ fwd.proto }}" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/firewall/config', document.getElementById('forward-rows'), renderForwardsFromConfig); showSuccessToast('Rule removed'); }">
|
|
<button type="submit" class="btn btn-sm btn-danger" hx-confirm="Remove forward rule {{ fwd.port }}/{{ fwd.proto }} → {{ fwd.toaddr }}:{{ fwd.toport }}?">Remove</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if not all_forwards %}
|
|
<tr>
|
|
<td colspan="6" class="text-muted text-sm">No port forwarding rules configured</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% endblock %}
|