Add live firewall rule management per server (nftables)
New model.FirewallRule + jsondb CRUD (GetFirewallRules/CreateFirewallRule/ UpdateFirewallRule/DeleteFirewallRule), scoped per server. firewall package now generates a full ruleset (baseline + enabled custom rules) and can apply it live via `nft -f` (firewall.Apply), scoped to a per-server nftables table (wireguard_ui_<serverID>) so applying one server never touches another server's rules or any pre-existing firewall state. New endpoints: GET/POST /servers/:id/firewall/rules, POST .../rules/:ruleId, POST .../rules/:ruleId/delete, POST .../apply (live, admin-only). UI in the All Servers page: rule table with add/delete, ruleset preview, and an "Apply now (live)" button with an explicit confirm() warning before it touches the running firewall. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
28eb08df41
commit
1d080904b0
+165
-15
@@ -176,21 +176,55 @@ All Servers
|
||||
</div>
|
||||
<!-- /.modal -->
|
||||
|
||||
<div class="modal fade" id="modal_firewall_preview">
|
||||
<div class="modal fade" id="modal_firewall">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Firewall Preview (nftables)</h4>
|
||||
<h4 class="modal-title">Firewall Rules (nftables) - <span id="_fw_server_label"></span></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted">Preview only - nothing is applied to the running firewall. Review, then apply manually if desired.</p>
|
||||
<pre id="_firewall_preview_text" style="max-height: 50vh; overflow:auto;"></pre>
|
||||
<input type="hidden" id="_fw_server_id">
|
||||
|
||||
<table class="table table-sm" id="_fw_rules_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>On</th><th>Chain</th><th>Proto</th><th>Port</th><th>Source</th><th>Action</th><th>Comment</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="_fw_rules_tbody"></tbody>
|
||||
</table>
|
||||
|
||||
<form id="frm_firewall_rule" class="form-inline">
|
||||
<select class="form-control form-control-sm mr-1 mb-1" id="_fw_direction">
|
||||
<option value="input">input</option>
|
||||
<option value="forward">forward</option>
|
||||
</select>
|
||||
<select class="form-control form-control-sm mr-1 mb-1" id="_fw_protocol">
|
||||
<option value="">any</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
<input type="text" class="form-control form-control-sm mr-1 mb-1" id="_fw_port" placeholder="port(s) e.g. 8080" style="width:9em">
|
||||
<input type="text" class="form-control form-control-sm mr-1 mb-1" id="_fw_source" placeholder="source CIDR (optional)" style="width:11em">
|
||||
<select class="form-control form-control-sm mr-1 mb-1" id="_fw_action">
|
||||
<option value="accept">accept</option>
|
||||
<option value="drop">drop</option>
|
||||
<option value="reject">reject</option>
|
||||
</select>
|
||||
<input type="text" class="form-control form-control-sm mr-1 mb-1" id="_fw_comment" placeholder="comment" style="width:11em">
|
||||
<button type="submit" class="btn btn-primary btn-sm mb-1">Add rule</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
<p class="text-muted mb-1">Ruleset preview:</p>
|
||||
<pre id="_firewall_preview_text" style="max-height: 30vh; overflow:auto;"></pre>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-danger" id="btn_apply_firewall">Apply now (live)</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
@@ -232,7 +266,7 @@ All Servers
|
||||
data-target="#modal_server_interface" data-serverid="${obj.id}">Interface</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-firewall-preview" data-serverid="${obj.id}">Firewall Preview</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-firewall-preview" data-serverid="${obj.id}" data-servername="${safeName}">Firewall</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm btn-delete-server" data-serverid="${obj.id}" data-servername="${safeName}">Delete</button>
|
||||
@@ -507,22 +541,138 @@ All Servers
|
||||
});
|
||||
});
|
||||
|
||||
// Firewall preview button: rendered dynamically, use event delegation
|
||||
// Firewall management modal
|
||||
function refreshFirewallPreview(serverId) {
|
||||
$("#_firewall_preview_text").text("Loading...");
|
||||
$.ajax({
|
||||
cache: false,
|
||||
method: 'GET',
|
||||
url: '{{.basePath}}/servers/' + serverId + '/firewall-preview',
|
||||
dataType: 'text',
|
||||
success: function (data) { $("#_firewall_preview_text").text(data); },
|
||||
error: function () { $("#_firewall_preview_text").text("Could not load firewall preview."); }
|
||||
});
|
||||
}
|
||||
|
||||
function renderFirewallRules(serverId, rules) {
|
||||
const tbody = $("#_fw_rules_tbody");
|
||||
tbody.empty();
|
||||
$.each(rules, function (i, rule) {
|
||||
const safeComment = $('<div>').text(rule.comment || "").html();
|
||||
const row = `<tr data-ruleid="${rule.id}">
|
||||
<td>${rule.enabled ? "yes" : "no"}</td>
|
||||
<td>${rule.direction}</td>
|
||||
<td>${rule.protocol || "any"}</td>
|
||||
<td>${rule.port || "any"}</td>
|
||||
<td>${rule.source || "any"}</td>
|
||||
<td>${rule.action}</td>
|
||||
<td>${safeComment}</td>
|
||||
<td><button type="button" class="btn btn-outline-danger btn-sm btn-delete-fw-rule" data-serverid="${serverId}" data-ruleid="${rule.id}">Delete</button></td>
|
||||
</tr>`;
|
||||
tbody.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function loadFirewallRules(serverId) {
|
||||
$.ajax({
|
||||
cache: false,
|
||||
method: 'GET',
|
||||
url: '{{.basePath}}/servers/' + serverId + '/firewall/rules',
|
||||
dataType: 'json',
|
||||
success: function (rules) { renderFirewallRules(serverId, rules); },
|
||||
error: function (jqXHR) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
toastr.error(responseJson['message']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Firewall button: rendered dynamically, use event delegation
|
||||
$(document).ready(function () {
|
||||
$('#servers-list').on('click', '.btn-firewall-preview', function () {
|
||||
const serverId = $(this).data('serverid');
|
||||
$("#_firewall_preview_text").text("Loading...");
|
||||
$("#modal_firewall_preview").modal('show');
|
||||
const serverName = $(this).data('servername');
|
||||
$("#_fw_server_id").val(serverId);
|
||||
$("#_fw_server_label").text(serverName + " (" + serverId + ")");
|
||||
$("#modal_firewall").modal('show');
|
||||
loadFirewallRules(serverId);
|
||||
refreshFirewallPreview(serverId);
|
||||
});
|
||||
|
||||
$("#frm_firewall_rule").on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const serverId = $("#_fw_server_id").val();
|
||||
const data = {
|
||||
direction: $("#_fw_direction").val(),
|
||||
protocol: $("#_fw_protocol").val(),
|
||||
port: $("#_fw_port").val(),
|
||||
source: $("#_fw_source").val(),
|
||||
action: $("#_fw_action").val(),
|
||||
comment: $("#_fw_comment").val(),
|
||||
enabled: true
|
||||
};
|
||||
$.ajax({
|
||||
cache: false,
|
||||
method: 'GET',
|
||||
url: '{{.basePath}}/servers/' + serverId + '/firewall-preview',
|
||||
dataType: 'text',
|
||||
success: function (data) {
|
||||
$("#_firewall_preview_text").text(data);
|
||||
method: 'POST',
|
||||
url: '{{.basePath}}/servers/' + serverId + '/firewall/rules',
|
||||
dataType: 'json',
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: function () {
|
||||
toastr.success("Rule added");
|
||||
$("#frm_firewall_rule")[0].reset();
|
||||
loadFirewallRules(serverId);
|
||||
refreshFirewallPreview(serverId);
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
$("#_firewall_preview_text").text("Could not load firewall preview.");
|
||||
error: function (jqXHR) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
toastr.error(responseJson['message']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#_fw_rules_tbody").on('click', '.btn-delete-fw-rule', function () {
|
||||
const serverId = $(this).data('serverid');
|
||||
const ruleId = $(this).data('ruleid');
|
||||
if (!confirm("Delete this firewall rule?")) return;
|
||||
$.ajax({
|
||||
cache: false,
|
||||
method: 'POST',
|
||||
url: '{{.basePath}}/servers/' + serverId + '/firewall/rules/' + ruleId + '/delete',
|
||||
dataType: 'json',
|
||||
contentType: "application/json",
|
||||
success: function () {
|
||||
toastr.success("Rule deleted");
|
||||
loadFirewallRules(serverId);
|
||||
refreshFirewallPreview(serverId);
|
||||
},
|
||||
error: function (jqXHR) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
toastr.error(responseJson['message']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_apply_firewall").click(function () {
|
||||
const serverId = $("#_fw_server_id").val();
|
||||
if (!confirm("Apply this ruleset to the live firewall now?\n" +
|
||||
"This runs 'nft -f' on the server, scoped to this server's own nftables table only.")) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
cache: false,
|
||||
method: 'POST',
|
||||
url: '{{.basePath}}/servers/' + serverId + '/firewall/apply',
|
||||
dataType: 'json',
|
||||
contentType: "application/json",
|
||||
success: function (data) {
|
||||
toastr.success(data.message);
|
||||
if (data.output) { $("#_firewall_preview_text").text(data.output); }
|
||||
},
|
||||
error: function (jqXHR) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
toastr.error(responseJson['message'] || "Failed to apply firewall rules");
|
||||
if (responseJson['output']) { $("#_firewall_preview_text").text(responseJson['output']); }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user