feat(firewall): nftables hit-counter pro Regel + Live-Hits-Spalte in der UI
- ruleset.nft.tpl: operator-rules bekommen `counter <action> comment "egid:<id>"`
→ nft zählt Packets + Bytes per Regel ab dem letzten Ruleset-Apply
- handlers/firewall_counters.go: GET /firewall/counters parst
`sudo nft list table inet edgeguard` per Regex, liefert [{rule_id,packets,bytes}]
- handlers/firewall_counters_test.go: unit-tests für parseNFTCounters
- Firewall/Rules.tsx: neue "Hits"-Spalte (Packet-Count, Tooltip mit Bytes),
10s-Polling via TanStack Query ['fw','counters']
- i18n de+en: fw.rule.hits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,7 @@ func (h *FirewallHandler) Register(rg *gin.RouterGroup) {
|
||||
g := rg.Group("/firewall")
|
||||
|
||||
g.GET("/auto-rules", h.AutoRules)
|
||||
g.GET("/counters", h.Counters)
|
||||
|
||||
zn := g.Group("/zones")
|
||||
zn.GET("", h.ListZone)
|
||||
|
||||
63
internal/handlers/firewall_counters.go
Normal file
63
internal/handlers/firewall_counters.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
||||
)
|
||||
|
||||
// ruleCounter is one operator-defined firewall rule's live packet/byte
|
||||
// counter. The nft ruleset template marks each rule with
|
||||
// `counter … comment "egid:<id>"` so we can correlate the counter
|
||||
// values back to DB rule IDs.
|
||||
type ruleCounter struct {
|
||||
RuleID int64 `json:"rule_id"`
|
||||
Packets int64 `json:"packets"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
}
|
||||
|
||||
var (
|
||||
reEgid = regexp.MustCompile(`comment "egid:(\d+)"`)
|
||||
reCounter = regexp.MustCompile(`\bcounter packets (\d+) bytes (\d+)\b`)
|
||||
)
|
||||
|
||||
// Counters reads live hit-counters from the running nft ruleset.
|
||||
// Runs `sudo -n /usr/sbin/nft list table inet edgeguard` (already
|
||||
// allowed by the postinst sudoers rule) and parses each rule line
|
||||
// for the `counter packets N bytes M … comment "egid:X"` pattern
|
||||
// that the ruleset template emits.
|
||||
func (h *FirewallHandler) Counters(c *gin.Context) {
|
||||
out, err := exec.Command("sudo", "-n", "/usr/sbin/nft", "list", "table", "inet", "edgeguard").CombinedOutput()
|
||||
if err != nil {
|
||||
response.OK(c, gin.H{
|
||||
"counters": []ruleCounter{},
|
||||
"error": strings.TrimSpace(string(out)),
|
||||
})
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"counters": parseNFTCounters(string(out))})
|
||||
}
|
||||
|
||||
func parseNFTCounters(output string) []ruleCounter {
|
||||
var out []ruleCounter
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
mID := reEgid.FindStringSubmatch(line)
|
||||
mCnt := reCounter.FindStringSubmatch(line)
|
||||
if mID == nil || mCnt == nil {
|
||||
continue
|
||||
}
|
||||
id, err1 := strconv.ParseInt(mID[1], 10, 64)
|
||||
pkts, err2 := strconv.ParseInt(mCnt[1], 10, 64)
|
||||
byts, err3 := strconv.ParseInt(mCnt[2], 10, 64)
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, ruleCounter{RuleID: id, Packets: pkts, Bytes: byts})
|
||||
}
|
||||
return out
|
||||
}
|
||||
70
internal/handlers/firewall_counters_test.go
Normal file
70
internal/handlers/firewall_counters_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseNFTCounters_ParsesCounterAndEgid(t *testing.T) {
|
||||
// nft list output shows `counter packets N bytes M` with actual values
|
||||
// (zero when unused). Template emits `counter <action> comment "egid:X"`;
|
||||
// nft expands that to `counter packets N bytes M <action> comment "egid:X"`.
|
||||
input := `
|
||||
table inet edgeguard {
|
||||
chain input {
|
||||
type filter hook input priority 0; policy drop;
|
||||
tcp dport 22 ct state new limit rate 10/minute accept comment "anti-lockout: SSH"
|
||||
tcp dport 80 counter packets 0 bytes 0 accept comment "egid:1"
|
||||
ip saddr 10.0.0.0/8 tcp dport 443 counter packets 1234 bytes 567890 accept comment "egid:2"
|
||||
ip saddr 1.2.3.4 drop comment "egid:3"
|
||||
tcp dport 8080 log prefix "edgeguard:4 " group 0 counter packets 7 bytes 420 reject comment "egid:4"
|
||||
}
|
||||
}
|
||||
`
|
||||
counters := parseNFTCounters(input)
|
||||
|
||||
if len(counters) != 3 {
|
||||
t.Fatalf("expected 3 counters (egid:1,2,4), got %d: %+v", len(counters), counters)
|
||||
}
|
||||
|
||||
byID := map[int64]ruleCounter{}
|
||||
for _, c := range counters {
|
||||
byID[c.RuleID] = c
|
||||
}
|
||||
|
||||
// egid:1 — zero counters are valid
|
||||
if c, ok := byID[1]; !ok {
|
||||
t.Error("missing counter for egid:1")
|
||||
} else if c.Packets != 0 || c.Bytes != 0 {
|
||||
t.Errorf("egid:1 want packets=0 bytes=0, got packets=%d bytes=%d", c.Packets, c.Bytes)
|
||||
}
|
||||
|
||||
// egid:2 — non-zero counters
|
||||
if c, ok := byID[2]; !ok {
|
||||
t.Error("missing counter for egid:2")
|
||||
} else if c.Packets != 1234 || c.Bytes != 567890 {
|
||||
t.Errorf("egid:2 want packets=1234 bytes=567890, got packets=%d bytes=%d", c.Packets, c.Bytes)
|
||||
}
|
||||
|
||||
// egid:3 — line has no counter keyword → must not appear
|
||||
if _, ok := byID[3]; ok {
|
||||
t.Error("egid:3 has no counter statement and must not appear in output")
|
||||
}
|
||||
|
||||
// egid:4 — counter + log + reject
|
||||
if _, ok := byID[4]; !ok {
|
||||
t.Error("missing counter for egid:4")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNFTCounters_EmptyOutput(t *testing.T) {
|
||||
if counters := parseNFTCounters(""); len(counters) != 0 {
|
||||
t.Errorf("expected empty result, got %+v", counters)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNFTCounters_NoRules(t *testing.T) {
|
||||
input := "table inet edgeguard {\n chain input {\n }\n}\n"
|
||||
if counters := parseNFTCounters(input); len(counters) != 0 {
|
||||
t.Errorf("expected empty result for table with no operator rules, got %+v", counters)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user