From 39133d0a76c5f7db324f781d2bf2a2123e4c74b6 Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 11:50:53 +0000 Subject: [PATCH] feat: collect new up metric from fail2ban socket Add support for connecting the exporter directly to the fail2ban server's socket to send requests and receive data. The path to the socket file is optional and specified on startup. Export a new metric based on the response of the `ping` command sent to the fail2ban server. The metric is set to 1 if the server responds with `pong` and 0 in any other case. This metric is only shown if the path to the socket file was provided on startup. --- src/cfg/cfg.go | 12 ++++--- src/exporter.go | 66 ++++++++++++++++++++++++++++-------- src/go.mod | 2 ++ src/go.sum | 4 +++ src/socket/decoder.go | 8 +++++ src/socket/fail2banSocket.go | 42 +++++++++++++++++++++++ src/socket/protocol.go | 53 +++++++++++++++++++++++++++++ 7 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 src/socket/decoder.go create mode 100644 src/socket/fail2banSocket.go create mode 100644 src/socket/protocol.go diff --git a/src/cfg/cfg.go b/src/cfg/cfg.go index e1ad5db..0731fdb 100644 --- a/src/cfg/cfg.go +++ b/src/cfg/cfg.go @@ -12,9 +12,10 @@ const ( ) type AppSettings struct { - VersionMode bool - MetricsPort int - Fail2BanDbPath string + VersionMode bool + MetricsPort int + Fail2BanDbPath string + Fail2BanSocketPath string } func Parse() *AppSettings { @@ -22,6 +23,7 @@ func Parse() *AppSettings { flag.BoolVar(&appSettings.VersionMode, "version", false, "show version info and exit") flag.IntVar(&appSettings.MetricsPort, "port", 9191, "port to use for the metrics server") flag.StringVar(&appSettings.Fail2BanDbPath, "db", "", "path to the fail2ban sqlite database") + flag.StringVar(&appSettings.Fail2BanSocketPath, "socket", "", "path to the fail2ban server socket") flag.Parse() appSettings.validateFlags() @@ -31,8 +33,8 @@ func Parse() *AppSettings { func (settings *AppSettings) validateFlags() { var flagsValid = true if !settings.VersionMode { - if settings.Fail2BanDbPath == "" { - fmt.Println("missing flag 'db'") + if settings.Fail2BanDbPath == "" && settings.Fail2BanSocketPath == "" { + fmt.Println("at least one of the following flags must be provided: 'db', 'socket'") flagsValid = false } if settings.MetricsPort < minServerPort || settings.MetricsPort > maxServerPort { diff --git a/src/exporter.go b/src/exporter.go index 6cc3ad1..5d568c3 100644 --- a/src/exporter.go +++ b/src/exporter.go @@ -3,15 +3,20 @@ package main import ( "fail2ban-prometheus-exporter/cfg" fail2banDb "fail2ban-prometheus-exporter/db" + "fail2ban-prometheus-exporter/socket" "fmt" + "log" + "net/http" + _ "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - "log" - "net/http" ) -const namespace = "fail2ban" +const ( + namespace = "fail2ban" + sockNamespace = "f2b" +) var ( version = "dev" @@ -44,28 +49,44 @@ var ( "Number of errors found since startup.", []string{"type"}, nil, ) + metricServerPing = prometheus.NewDesc( + prometheus.BuildFQName(sockNamespace, "", "up"), + "Check if the fail2ban server is up", + nil, nil, + ) ) type Exporter struct { db *fail2banDb.Fail2BanDB + socket *socket.Fail2BanSocket lastError error dbErrorCount int } func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - ch <- metricUp - ch <- metricBadIpsPerJail - ch <- metricBannedIpsPerJail - ch <- metricEnabledJails - ch <- metricErrorCount + if e.db != nil { + ch <- metricUp + ch <- metricBadIpsPerJail + ch <- metricBannedIpsPerJail + ch <- metricEnabledJails + ch <- metricErrorCount + } + if e.socket != nil { + ch <- metricServerPing + } } func (e *Exporter) Collect(ch chan<- prometheus.Metric) { - e.collectBadIpsPerJailMetrics(ch) - e.collectBannedIpsPerJailMetrics(ch) - e.collectEnabledJailMetrics(ch) - e.collectUpMetric(ch) - e.collectErrorCountMetric(ch) + if e.db != nil { + e.collectBadIpsPerJailMetrics(ch) + e.collectBannedIpsPerJailMetrics(ch) + e.collectEnabledJailMetrics(ch) + e.collectUpMetric(ch) + e.collectErrorCountMetric(ch) + } + if e.socket != nil { + e.collectServerPingMetric(ch) + } } func (e *Exporter) collectUpMetric(ch chan<- prometheus.Metric) { @@ -132,6 +153,17 @@ func (e *Exporter) collectEnabledJailMetrics(ch chan<- prometheus.Metric) { } } +func (e *Exporter) collectServerPingMetric(ch chan<- prometheus.Metric) { + pingSuccess := e.socket.Ping() + var pingSuccessInt float64 = 1 + if !pingSuccess { + pingSuccessInt = 0 + } + ch <- prometheus.MustNewConstMetric( + metricServerPing, prometheus.GaugeValue, pingSuccessInt, + ) +} + func printAppVersion() { fmt.Println(version) fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy) @@ -144,8 +176,12 @@ func main() { } else { log.Print("starting fail2ban exporter") - exporter := &Exporter{ - db: fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath), + exporter := &Exporter{} + if appSettings.Fail2BanDbPath != "" { + exporter.db = fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath) + } + if appSettings.Fail2BanSocketPath != "" { + exporter.socket = socket.MustConnectToSocket(appSettings.Fail2BanSocketPath) } prometheus.MustRegister(exporter) diff --git a/src/go.mod b/src/go.mod index 2df78ea..1e47f44 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,6 +3,8 @@ module fail2ban-prometheus-exporter go 1.15 require ( + github.com/kisielk/og-rek v1.1.0 github.com/mattn/go-sqlite3 v1.14.6 + github.com/nlpodyssey/gopickle v0.1.0 github.com/prometheus/client_golang v1.9.0 ) diff --git a/src/go.sum b/src/go.sum index 7de5c32..8f79c52 100644 --- a/src/go.sum +++ b/src/go.sum @@ -137,6 +137,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kisielk/og-rek v1.1.0 h1:u10TvQbPtrlY/6H4+BiFsBywwSVTGFsx0YOVtpx3IbI= +github.com/kisielk/og-rek v1.1.0/go.mod h1:6ihsOSzSAxR/65S3Bn9zNihoEqRquhDQZ2c6I2+MG3c= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -175,6 +177,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nlpodyssey/gopickle v0.1.0 h1:9wjwRqXsOSYWZl4c4ko472b6RW+VB1I441ZcfFg1r5g= +github.com/nlpodyssey/gopickle v0.1.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= diff --git a/src/socket/decoder.go b/src/socket/decoder.go new file mode 100644 index 0000000..41f1ac4 --- /dev/null +++ b/src/socket/decoder.go @@ -0,0 +1,8 @@ +package socket + +// Py_builtins_str is used by the pickle decoder to parse the server response into a format Go can understand +type Py_builtins_str struct{} + +func (c Py_builtins_str) Call(args ...interface{}) (interface{}, error) { + return args[0], nil +} diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go new file mode 100644 index 0000000..35513c8 --- /dev/null +++ b/src/socket/fail2banSocket.go @@ -0,0 +1,42 @@ +package socket + +import ( + "log" + "net" + + "github.com/kisielk/og-rek" + "github.com/nlpodyssey/gopickle/types" +) + +type Fail2BanSocket struct { + socket net.Conn + encoder *ogórek.Encoder +} + +func MustConnectToSocket(path string) *Fail2BanSocket { + c, err := net.Dial("unix", path) + if err != nil { + log.Fatalf("failed to open fail2ban socket: %v", err) + } + return &Fail2BanSocket{ + socket: c, + encoder: ogórek.NewEncoder(c), + } +} + +func (s *Fail2BanSocket) Ping() bool { + response, err := s.sendCommand([]string{pingCommand, "100"}) + if err != nil { + log.Printf("server ping failed: %v", err) + return false + } + + if t, ok := response.(*types.Tuple); ok { + if (*t)[1] == "pong" { + return true + } + log.Printf("unexpected response data: %s", t) + } + log.Printf("unexpected response format - cannot parse: %v", response) + return false +} diff --git a/src/socket/protocol.go b/src/socket/protocol.go new file mode 100644 index 0000000..9dc10f5 --- /dev/null +++ b/src/socket/protocol.go @@ -0,0 +1,53 @@ +package socket + +import ( + "bytes" + "fmt" + "github.com/nlpodyssey/gopickle/pickle" +) + +const ( + commandTerminator = "" + pingCommand = "ping" + socketReadBufferSize = 10000 +) + +func (s *Fail2BanSocket) sendCommand(command []string) (interface{}, error) { + err := s.write(command) + if err != nil { + return nil, err + } + return s.read() +} + +func (s *Fail2BanSocket) write(command []string) error { + err := s.encoder.Encode(command) + if err != nil { + return err + } + _, err = s.socket.Write([]byte(commandTerminator)) + if err != nil { + return err + } + return nil +} + +func (s *Fail2BanSocket) read() (interface{}, error) { + buf := make([]byte, socketReadBufferSize) + _, err := s.socket.Read(buf) + if err != nil { + return nil, err + } + + bufReader := bytes.NewReader(buf) + unpickler := pickle.NewUnpickler(bufReader) + + unpickler.FindClass = func(module, name string) (interface{}, error) { + if module == "builtins" && name == "str" { + return &Py_builtins_str{}, nil + } + return nil, fmt.Errorf("class not found: " + module + " : " + name) + } + + return unpickler.Load() +}