basic PF prometheus metrics (packets, bytes, banned ip count) (#349)

This commit is contained in:
mmetc
2024-01-19 15:56:22 +01:00
committed by GitHub
parent 7f706b66de
commit eb7094b1f9
8 changed files with 190 additions and 10 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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
View 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)
}

View File

@@ -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

View File

@@ -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*",
])

View File

@@ -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 . . .*",