diff --git a/firewall/apply.go b/firewall/apply.go index 70d0e14..3174054 100644 --- a/firewall/apply.go +++ b/firewall/apply.go @@ -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 +} diff --git a/handler/routes.go b/handler/routes.go index 6f4559a..0dc5ee7 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -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 diff --git a/main.go b/main.go index b9c7794..89d6ab2 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/templates/firewall_lists.html b/templates/firewall_lists.html index b8b4849..3263f89 100644 --- a/templates/firewall_lists.html +++ b/templates/firewall_lists.html @@ -21,6 +21,9 @@ Global Firewall Lists
@@ -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)".
+