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) {
|
||||
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
|
||||
// live via `nft -f`, scoped to firewall.GlobalTableName only. Runs at
|
||||
// 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.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.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+"/favicon", handler.Favicon())
|
||||
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-header">
|
||||
<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 class="card-body">
|
||||
<p class="text-muted">
|
||||
@@ -29,6 +32,7 @@ Global Firewall Lists
|
||||
firewall table (nftables priority -10). Allow entries always win over block entries.
|
||||
Nothing is applied until you press "Apply now (live)".
|
||||
</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">
|
||||
<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 () {
|
||||
loadIPList();
|
||||
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) {
|
||||
e.preventDefault();
|
||||
@@ -226,6 +290,7 @@ Global Firewall Lists
|
||||
success: function (data) {
|
||||
toastr.success(data.message);
|
||||
if (data.output) { $("#_iplist_preview_text").text(data.output); }
|
||||
refreshGlobalStatus();
|
||||
},
|
||||
error: function (jqXHR) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
|
||||
Reference in New Issue
Block a user