Add central host-wide firewall allow/block lists
New model.IPListEntry + jsondb CRUD, independent of any single WireGuard server. firewall.GenerateGlobalRuleset builds an nftables table (wireguard_ui_global) with allow/block sets evaluated at priority -10 - before every per-server table - so it applies to all traffic on the host, not just WireGuard. Allow entries always win over block entries. firewall.ApplyGlobal loads it live via `nft -f`, scoped to that one table. New "Global Firewall Lists" page (nav entry under Settings): add/delete entries, ruleset preview, "Apply now (live)" with an explicit confirm() warning since this affects the whole host's firewall, not just one server. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
bdcf1ec60c
commit
eb1913d400
@@ -1779,3 +1779,107 @@ func GetSystemUpdateStatus() echo.HandlerFunc {
|
||||
return c.JSON(http.StatusOK, system.CheckAptUpdates())
|
||||
}
|
||||
}
|
||||
|
||||
// FirewallListsPage renders the host-wide allow/block list management page.
|
||||
func FirewallListsPage() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "firewall_lists.html", map[string]interface{}{
|
||||
"baseData": model.BaseData{Active: "firewall-lists", CurrentUser: currentUser(c), Admin: isAdmin(c)},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validateIPListEntry(entry model.IPListEntry) error {
|
||||
if entry.ListType != "allow" && entry.ListType != "block" {
|
||||
return fmt.Errorf("list_type must be 'allow' or 'block'")
|
||||
}
|
||||
if !util.ValidateServerAddresses([]string{entry.CIDR}) && net.ParseIP(entry.CIDR) == nil {
|
||||
return fmt.Errorf("cidr must be a valid IP or CIDR")
|
||||
}
|
||||
if len(entry.Comment) > 200 {
|
||||
return fmt.Errorf("comment too long")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIPListEntries lists every host-wide allow/block list entry.
|
||||
func GetIPListEntries(db store.IStore) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
entries, err := db.GetIPListEntries()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, entries)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateIPListEntryHandler adds a new host-wide allow/block list entry.
|
||||
func CreateIPListEntryHandler(db store.IStore) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var entry model.IPListEntry
|
||||
if err := c.Bind(&entry); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
|
||||
}
|
||||
if err := validateIPListEntry(entry); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, err.Error()})
|
||||
}
|
||||
entry.ID = xid.New().String()
|
||||
entry.CreatedAt = time.Now().UTC()
|
||||
if err := db.CreateIPListEntry(entry); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Cannot create entry: %v", err)})
|
||||
}
|
||||
return c.JSON(http.StatusOK, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteIPListEntryHandler removes a host-wide allow/block list entry.
|
||||
func DeleteIPListEntryHandler(db store.IStore) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
if err := db.DeleteIPListEntry(id); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Entry deleted successfully"})
|
||||
}
|
||||
}
|
||||
|
||||
// GetGlobalFirewallPreview returns the generated host-wide allow/block list
|
||||
// ruleset as plain text. Preview only.
|
||||
func GetGlobalFirewallPreview(db store.IStore) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
entries, err := db.GetIPListEntries()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
|
||||
}
|
||||
return c.String(http.StatusOK, firewall.GenerateGlobalRuleset(entries))
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// applies to all traffic on the host.
|
||||
func ApplyGlobalFirewallHandler(db store.IStore) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
entries, err := db.GetIPListEntries()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
|
||||
}
|
||||
ruleset := firewall.GenerateGlobalRuleset(entries)
|
||||
output, err := firewall.ApplyGlobal(ruleset)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to apply global firewall lists: %v\n%s", err, output)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
"output": output,
|
||||
})
|
||||
}
|
||||
log.Infof("Applied global firewall allow/block lists")
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Global firewall lists applied successfully",
|
||||
"output": output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user