mirror of
https://github.com/crowdsecurity/cs-firewall-bouncer.git
synced 2024-08-19 01:18:49 +03:00
basic PF prometheus metrics (packets, bytes, banned ip count) (#349)
This commit is contained in:
@@ -213,7 +213,7 @@ func Execute() error {
|
||||
})
|
||||
|
||||
if config.PrometheusConfig.Enabled {
|
||||
if config.Mode == cfg.IptablesMode || config.Mode == cfg.NftablesMode {
|
||||
if config.Mode == cfg.IptablesMode || config.Mode == cfg.NftablesMode || config.Mode == cfg.PfMode {
|
||||
go backend.CollectMetrics()
|
||||
prometheus.MustRegister(metrics.TotalDroppedBytes, metrics.TotalDroppedPackets, metrics.TotalActiveBannedIPs)
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/google/nftables v0.1.1-0.20230710063801-8a10f689006b
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/sys v0.13.0
|
||||
@@ -22,6 +23,7 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/go-openapi/analysis v0.21.4 // indirect
|
||||
github.com/go-openapi/errors v0.20.4 // indirect
|
||||
@@ -46,6 +48,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
|
||||
@@ -41,6 +41,7 @@ func (b *BackendCTX) Delete(decision *models.Decision) error {
|
||||
}
|
||||
|
||||
func (b *BackendCTX) CollectMetrics() {
|
||||
log.Trace("Collecting backend-specific metrics")
|
||||
b.firewall.CollectMetrics()
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ func NewBackend(config *cfg.BouncerConfig) (*BackendCTX, error) {
|
||||
|
||||
b := &BackendCTX{}
|
||||
|
||||
log.Printf("backend type : %s", config.Mode)
|
||||
log.Printf("backend type: %s", config.Mode)
|
||||
|
||||
if config.DisableIPV6 {
|
||||
log.Println("IPV6 is disabled")
|
||||
|
||||
144
pkg/pf/metrics.go
Normal file
144
pkg/pf/metrics.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package pf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/metrics"
|
||||
)
|
||||
|
||||
type counter struct {
|
||||
packets int
|
||||
bytes int
|
||||
}
|
||||
|
||||
var (
|
||||
// table names can contain _ or - characters.
|
||||
rexpTable = regexp.MustCompile(`^block .* from <(?P<table>[^ ]+)> .*"$`)
|
||||
rexpMetrics = regexp.MustCompile(`^\s+\[.*Packets: (?P<packets>\d+)\s+Bytes: (?P<bytes>\d+).*\]$`)
|
||||
)
|
||||
|
||||
func parseMetrics(reader *strings.Reader, tables []string) map[string]counter {
|
||||
ret := make(map[string]counter)
|
||||
|
||||
// scan until we find a table name between <>
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// parse the line and extract the table name
|
||||
match := rexpTable.FindStringSubmatch(line)
|
||||
if len(match) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
table := match[1]
|
||||
// if the table is not in the list of tables we want to parse, skip it
|
||||
if !slices.Contains(tables, table) {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the line with the actual metrics
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
|
||||
line = scanner.Text()
|
||||
|
||||
match = rexpMetrics.FindStringSubmatch(line)
|
||||
if len(match) == 0 {
|
||||
log.Errorf("failed to parse metrics: %s", line)
|
||||
continue
|
||||
}
|
||||
|
||||
packets, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse metrics - dropped packets: %s", err)
|
||||
|
||||
packets = 0
|
||||
}
|
||||
|
||||
bytes, err := strconv.Atoi(match[2])
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse metrics - dropped bytes: %s", err)
|
||||
|
||||
bytes = 0
|
||||
}
|
||||
|
||||
ret[table] = counter{
|
||||
packets: packets,
|
||||
bytes: bytes,
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// countIPs returns the number of IPs in a table.
|
||||
func (pf *pf) countIPs(table string) int {
|
||||
cmd := execPfctl("", "-T", "show", "-t", table)
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Errorf("failed to run 'pfctl -T show -t %s': %s", table, err)
|
||||
return 0
|
||||
}
|
||||
|
||||
// one IP per line
|
||||
return strings.Count(string(out), "\n")
|
||||
}
|
||||
|
||||
// CollectMetrics collects metrics from pfctl.
|
||||
// In pf mode the firewall rules are not controlled by the bouncer, so we can only
|
||||
// trust they are set up correctly, and retrieve stats from the pfctl tables.
|
||||
func (pf *pf) CollectMetrics() {
|
||||
droppedPackets := float64(0)
|
||||
droppedBytes := float64(0)
|
||||
|
||||
tables := []string{}
|
||||
|
||||
if pf.inet != nil {
|
||||
tables = append(tables, pf.inet.table)
|
||||
}
|
||||
|
||||
if pf.inet6 != nil {
|
||||
tables = append(tables, pf.inet6.table)
|
||||
}
|
||||
|
||||
t := time.NewTicker(metrics.MetricCollectionInterval)
|
||||
|
||||
for range t.C {
|
||||
cmd := execPfctl("", "-v", "-sr")
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Errorf("failed to run 'pfctl -v -sr': %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
reader := strings.NewReader(string(out))
|
||||
stats := parseMetrics(reader, tables)
|
||||
bannedIPs := 0
|
||||
|
||||
for _, table := range tables {
|
||||
st, ok := stats[table]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
droppedPackets += float64(st.packets)
|
||||
droppedBytes += float64(st.bytes)
|
||||
|
||||
bannedIPs += pf.countIPs(table)
|
||||
}
|
||||
|
||||
metrics.TotalDroppedPackets.Set(droppedPackets)
|
||||
metrics.TotalDroppedBytes.Set(droppedBytes)
|
||||
metrics.TotalActiveBannedIPs.Set(float64(bannedIPs))
|
||||
}
|
||||
}
|
||||
34
pkg/pf/metrics_test.go
Normal file
34
pkg/pf/metrics_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package pf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseMetrics(t *testing.T) {
|
||||
metricsInput := `block drop in quick inet from <crowdsec_blacklists> to any label "CrowdSec IPv4"
|
||||
[ Evaluations: 1519 Packets: 16 Bytes: 4096 States: 0 ]
|
||||
[ Inserted: uid 0 pid 14219 State Creations: 0 ]
|
||||
block drop in quick inet6 from <crowdsec6_blacklists> to any label "CrowdSec IPv6"
|
||||
[ Evaluations: 914 Packets: 8 Bytes: 2048 States: 0 ]
|
||||
[ Inserted: uid 0 pid 14219 State Creations: 0 ]`
|
||||
|
||||
reader := strings.NewReader(metricsInput)
|
||||
tables := []string{"crowdsec_blacklists", "crowdsec6_blacklists"}
|
||||
|
||||
metrics := parseMetrics(reader, tables)
|
||||
|
||||
require.Contains(t, metrics, "crowdsec_blacklists")
|
||||
require.Contains(t, metrics, "crowdsec6_blacklists")
|
||||
|
||||
ip4Metrics := metrics["crowdsec_blacklists"]
|
||||
assert.Equal(t, 16, ip4Metrics.packets)
|
||||
assert.Equal(t, 4096, ip4Metrics.bytes)
|
||||
|
||||
ip6Metrics := metrics["crowdsec6_blacklists"]
|
||||
assert.Equal(t, 8, ip6Metrics.packets)
|
||||
assert.Equal(t, 2048, ip6Metrics.bytes)
|
||||
}
|
||||
@@ -59,7 +59,8 @@ func NewPF(config *cfg.BouncerConfig) (types.Backend, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// execPfctl runs a pfctl command by prepending the anchor name if we have one.
|
||||
// execPfctl runs a pfctl command by prepending the anchor name if needed.
|
||||
// Some commands return an error if an anchor is specified.
|
||||
func execPfctl(anchor string, arg ...string) *exec.Cmd {
|
||||
if anchor != "" {
|
||||
arg = append([]string{"-a", anchor}, arg...)
|
||||
@@ -178,9 +179,6 @@ func (pf *pf) commitAddedDecisions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pf *pf) CollectMetrics() {
|
||||
}
|
||||
|
||||
func (pf *pf) Delete(decision *models.Decision) error {
|
||||
pf.decisionsToDelete = append(pf.decisionsToDelete, decision)
|
||||
return nil
|
||||
|
||||
@@ -28,7 +28,7 @@ def test_backend_mode(bouncer, fw_cfg_factory):
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*Starting crowdsec-firewall-bouncer*",
|
||||
"*backend type : dry-run*",
|
||||
"*backend type: dry-run*",
|
||||
"*backend.Init() called*",
|
||||
"*unable to configure bouncer: config does not contain LAPI url*",
|
||||
])
|
||||
|
||||
@@ -33,7 +33,7 @@ def test_tls_server(crowdsec, certs_dir, api_key_factory, bouncer, fw_cfg_factor
|
||||
|
||||
with bouncer(cfg) as cb:
|
||||
cb.wait_for_lines_fnmatch([
|
||||
"*backend type : dry-run*",
|
||||
"*backend type: dry-run*",
|
||||
"*Using API key auth*",
|
||||
"*auth-api: auth with api key failed*",
|
||||
"*tls: failed to verify certificate: x509: certificate signed by unknown authority*",
|
||||
@@ -43,7 +43,7 @@ def test_tls_server(crowdsec, certs_dir, api_key_factory, bouncer, fw_cfg_factor
|
||||
|
||||
with bouncer(cfg) as cb:
|
||||
cb.wait_for_lines_fnmatch([
|
||||
"*backend type : dry-run*",
|
||||
"*backend type: dry-run*",
|
||||
"*Using CA cert *ca.crt*",
|
||||
"*Using API key auth*",
|
||||
"*Processing new and deleted decisions*",
|
||||
@@ -95,7 +95,7 @@ def test_tls_mutual(crowdsec, certs_dir, api_key_factory, bouncer, fw_cfg_factor
|
||||
|
||||
with bouncer(cfg) as cb:
|
||||
cb.wait_for_lines_fnmatch([
|
||||
"*backend type : dry-run*",
|
||||
"*backend type: dry-run*",
|
||||
"*Using CA cert*",
|
||||
"*Using cert auth with cert * and key *",
|
||||
"*Processing new and deleted decisions . . .*",
|
||||
|
||||
Reference in New Issue
Block a user