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:
sysops
2026-07-12 17:31:08 +02:00
co-authored by Claude Sonnet 5
parent 28eb08df41
commit 1d080904b0
8 changed files with 507 additions and 41 deletions
+59
View File
@@ -677,3 +677,62 @@ func (o *JsonDB) SaveServerHashes(serverID string, hashes model.ClientServerHash
}
return output
}
// GetFirewallRules func to query all firewall rules belonging to a server
func (o *JsonDB) GetFirewallRules(serverID string) ([]model.FirewallRule, error) {
if err := validateServerID(serverID); err != nil {
return nil, err
}
rules := make([]model.FirewallRule, 0)
records, err := o.conn.ReadAll("firewall_rules")
if err != nil {
if err == scribble.ErrMissingCollection {
return rules, nil
}
return nil, err
}
for _, rec := range records {
var rule model.FirewallRule
if err := json.Unmarshal(rec, &rule); err != nil {
return nil, fmt.Errorf("cannot decode firewall rule json structure: %v", err)
}
if rule.ServerID == serverID {
rules = append(rules, rule)
}
}
return rules, nil
}
// CreateFirewallRule func to add a new firewall rule for a server
func (o *JsonDB) CreateFirewallRule(rule model.FirewallRule) error {
if err := validateServerID(rule.ServerID); err != nil {
return err
}
return o.conn.Write("firewall_rules", rule.ID, rule)
}
// UpdateFirewallRule func to update an existing firewall rule, refusing to
// move it to a different server than it currently belongs to
func (o *JsonDB) UpdateFirewallRule(rule model.FirewallRule) error {
var existing model.FirewallRule
if err := o.conn.Read("firewall_rules", rule.ID, &existing); err != nil {
return fmt.Errorf("firewall rule not found: %v", err)
}
if existing.ServerID != rule.ServerID {
return fmt.Errorf("cannot change server_id of an existing firewall rule")
}
return o.conn.Write("firewall_rules", rule.ID, rule)
}
// DeleteFirewallRule func to remove a firewall rule, verifying it belongs to
// the given server first
func (o *JsonDB) DeleteFirewallRule(serverID, ruleID string) error {
var existing model.FirewallRule
if err := o.conn.Read("firewall_rules", ruleID, &existing); err != nil {
return fmt.Errorf("firewall rule not found: %v", err)
}
if existing.ServerID != serverID {
return fmt.Errorf("firewall rule does not belong to server %s", serverID)
}
return o.conn.Delete("firewall_rules", ruleID)
}
+4
View File
@@ -34,4 +34,8 @@ type IStore interface {
SaveServerHashes(serverID string, hashes model.ClientServerHashes) error
UpdateServerInterface(serverID string, serverInterface model.ServerInterface) error
UpdateServerKeyPair(serverID string, serverKeyPair model.ServerKeypair) error
GetFirewallRules(serverID string) ([]model.FirewallRule, error)
CreateFirewallRule(rule model.FirewallRule) error
UpdateFirewallRule(rule model.FirewallRule) error
DeleteFirewallRule(serverID, ruleID string) error
}