Add enable/disable toggle for the global firewall allow/block lists
New firewall.DisableGlobal() removes the wireguard_ui_global nftables table without touching stored IP list entries, and firewall.IsGlobalEnabled() reports whether it's currently loaded. New GET /firewall-lists/status and POST /firewall-lists/disable endpoints (admin-only), plus a status badge and "Toggle enable/disable" button on the Global Firewall Lists page - one click to turn the whole thing off without losing the list contents, and back on again (re-applies the current ruleset). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
00d084a188
commit
5a7709bc6e
@@ -52,3 +52,23 @@ func Apply(serverID, ruleset string) (string, error) {
|
|||||||
func ApplyGlobal(ruleset string) (string, error) {
|
func ApplyGlobal(ruleset string) (string, error) {
|
||||||
return applyTable(GlobalTableName, ruleset)
|
return applyTable(GlobalTableName, ruleset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableGlobal removes the host-wide allow/block list table entirely,
|
||||||
|
// turning enforcement off without touching the stored IP list entries -
|
||||||
|
// they stay in the database and can be re-applied later with ApplyGlobal.
|
||||||
|
// A missing table (already disabled) is not treated as an error.
|
||||||
|
func DisableGlobal() (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
out, _ := exec.CommandContext(ctx, "nft", "delete", "table", "inet", GlobalTableName).CombinedOutput()
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGlobalEnabled reports whether the host-wide allow/block list table is
|
||||||
|
// currently loaded in the live firewall.
|
||||||
|
func IsGlobalEnabled() bool {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
err := exec.CommandContext(ctx, "nft", "list", "table", "inet", GlobalTableName).Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2004,6 +2004,38 @@ func GetGlobalFirewallPreview(db store.IStore) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGlobalFirewallStatus reports whether the host-wide allow/block list
|
||||||
|
// table is currently loaded in the live firewall.
|
||||||
|
func GetGlobalFirewallStatus() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"enabled": firewall.IsGlobalEnabled(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableGlobalFirewallHandler removes the host-wide allow/block list table
|
||||||
|
// from the live firewall, without touching the stored entries.
|
||||||
|
func DisableGlobalFirewallHandler() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
output, err := firewall.DisableGlobal()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to disable global firewall: %v\n%s", err, output)
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
"output": output,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
log.Infof("Disabled global firewall allow/block lists")
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "Global firewall disabled",
|
||||||
|
"output": output,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ApplyGlobalFirewallHandler loads the host-wide allow/block list ruleset
|
// ApplyGlobalFirewallHandler loads the host-wide allow/block list ruleset
|
||||||
// live via `nft -f`, scoped to firewall.GlobalTableName only. Runs at
|
// live via `nft -f`, scoped to firewall.GlobalTableName only. Runs at
|
||||||
// priority -10, before every per-server WireGuard firewall table, so it
|
// priority -10, before every per-server WireGuard firewall table, so it
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ func main() {
|
|||||||
app.POST(util.BasePath+"/firewall-lists/entries/import", handler.BulkImportIPListEntries(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
|
app.POST(util.BasePath+"/firewall-lists/entries/import", handler.BulkImportIPListEntries(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
|
||||||
app.GET(util.BasePath+"/firewall-lists/preview", handler.GetGlobalFirewallPreview(db), handler.ValidSession, handler.NeedsAdmin)
|
app.GET(util.BasePath+"/firewall-lists/preview", handler.GetGlobalFirewallPreview(db), handler.ValidSession, handler.NeedsAdmin)
|
||||||
app.POST(util.BasePath+"/firewall-lists/apply", handler.ApplyGlobalFirewallHandler(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
|
app.POST(util.BasePath+"/firewall-lists/apply", handler.ApplyGlobalFirewallHandler(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
|
||||||
|
app.GET(util.BasePath+"/firewall-lists/status", handler.GetGlobalFirewallStatus(), handler.ValidSession, handler.NeedsAdmin)
|
||||||
|
app.POST(util.BasePath+"/firewall-lists/disable", handler.DisableGlobalFirewallHandler(), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
|
||||||
app.GET(util.BasePath+"/_health", handler.Health())
|
app.GET(util.BasePath+"/_health", handler.Health())
|
||||||
app.GET(util.BasePath+"/favicon", handler.Favicon())
|
app.GET(util.BasePath+"/favicon", handler.Favicon())
|
||||||
app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
|
app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ Global Firewall Lists
|
|||||||
<div class="card card-warning">
|
<div class="card card-warning">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">Host-wide Allow / Block Lists</h3>
|
<h3 class="card-title">Host-wide Allow / Block Lists</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<span id="_global_fw_status" class="badge">checking...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
@@ -29,6 +32,7 @@ Global Firewall Lists
|
|||||||
firewall table (nftables priority -10). Allow entries always win over block entries.
|
firewall table (nftables priority -10). Allow entries always win over block entries.
|
||||||
Nothing is applied until you press "Apply now (live)".
|
Nothing is applied until you press "Apply now (live)".
|
||||||
</p>
|
</p>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="btn_toggle_global_firewall">Toggle enable/disable</button>
|
||||||
|
|
||||||
<table class="table table-sm" id="_iplist_table">
|
<table class="table table-sm" id="_iplist_table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -126,9 +130,69 @@ Global Firewall Lists
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshGlobalStatus() {
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
method: 'GET',
|
||||||
|
url: '{{.basePath}}/firewall-lists/status',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (data) {
|
||||||
|
const badge = $("#_global_fw_status");
|
||||||
|
if (data.enabled) {
|
||||||
|
badge.removeClass('badge-secondary').addClass('badge-success').text('ENABLED');
|
||||||
|
} else {
|
||||||
|
badge.removeClass('badge-success').addClass('badge-secondary').text('DISABLED');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$("#_global_fw_status").removeClass('badge-success').addClass('badge-secondary').text('unknown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
loadIPList();
|
loadIPList();
|
||||||
refreshGlobalPreview();
|
refreshGlobalPreview();
|
||||||
|
refreshGlobalStatus();
|
||||||
|
|
||||||
|
$("#btn_toggle_global_firewall").click(function () {
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
method: 'GET',
|
||||||
|
url: '{{.basePath}}/firewall-lists/status',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (data) {
|
||||||
|
if (data.enabled) {
|
||||||
|
if (!confirm("Disable the global allow/block list firewall now?\nStored entries are kept, only the live nftables table is removed.")) return;
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST', url: '{{.basePath}}/firewall-lists/disable',
|
||||||
|
dataType: 'json', contentType: "application/json",
|
||||||
|
success: function (r) { toastr.success(r.message); refreshGlobalStatus(); },
|
||||||
|
error: function (jqXHR) {
|
||||||
|
const rj = jQuery.parseJSON(jqXHR.responseText);
|
||||||
|
toastr.error(rj['message'] || "Failed to disable");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!confirm("Enable the global allow/block list firewall now?\nThis applies the current ruleset live via 'nft -f'.")) return;
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST', url: '{{.basePath}}/firewall-lists/apply',
|
||||||
|
dataType: 'json', contentType: "application/json",
|
||||||
|
success: function (r) {
|
||||||
|
toastr.success(r.message);
|
||||||
|
if (r.output) { $("#_iplist_preview_text").text(r.output); }
|
||||||
|
refreshGlobalStatus();
|
||||||
|
},
|
||||||
|
error: function (jqXHR) {
|
||||||
|
const rj = jQuery.parseJSON(jqXHR.responseText);
|
||||||
|
toastr.error(rj['message'] || "Failed to enable");
|
||||||
|
if (rj['output']) { $("#_iplist_preview_text").text(rj['output']); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$("#frm_iplist_entry").on('submit', function (e) {
|
$("#frm_iplist_entry").on('submit', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -226,6 +290,7 @@ Global Firewall Lists
|
|||||||
success: function (data) {
|
success: function (data) {
|
||||||
toastr.success(data.message);
|
toastr.success(data.message);
|
||||||
if (data.output) { $("#_iplist_preview_text").text(data.output); }
|
if (data.output) { $("#_iplist_preview_text").text(data.output); }
|
||||||
|
refreshGlobalStatus();
|
||||||
},
|
},
|
||||||
error: function (jqXHR) {
|
error: function (jqXHR) {
|
||||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||||
|
|||||||
Reference in New Issue
Block a user