From 073ec89cb357cf9094c9e6683533d4c7f0b6b300 Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 10:11:17 +0100 Subject: [PATCH 1/7] new config flag for socket path --- src/cfg/cfg.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cfg/cfg.go b/src/cfg/cfg.go index e1ad5db..1321fbb 100644 --- a/src/cfg/cfg.go +++ b/src/cfg/cfg.go @@ -15,6 +15,7 @@ type AppSettings struct { 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 { From 86f8fd2c07a3f1af03b9d3a5e289c2d9d66daeef Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 10:44:12 +0100 Subject: [PATCH 2/7] support for sending commands over socket Add support for sending a ping command to the fail2ban server over the socket file. This includes encoding the command data using og-rek and decoding the response using `gopickle`. --- src/go.mod | 2 ++ src/go.sum | 4 +++ src/socket/decoder.go | 8 ++++++ src/socket/fail2banSocket.go | 38 +++++++++++++++++++++++++ src/socket/protocol.go | 55 ++++++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) 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/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..3df6d3c --- /dev/null +++ b/src/socket/fail2banSocket.go @@ -0,0 +1,38 @@ +package socket + +import ( + "github.com/kisielk/og-rek" + "log" + "net" +) + +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.NewEncoderWithConfig(c, &ogórek.EncoderConfig{ + Protocol: 5, + }), + } +} + +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 v, ok := response.([]string); ok && v[1] == "pong" { + return true + } + log.Printf("unexpected response from server: %v", response) + return false +} diff --git a/src/socket/protocol.go b/src/socket/protocol.go new file mode 100644 index 0000000..905cae1 --- /dev/null +++ b/src/socket/protocol.go @@ -0,0 +1,55 @@ +package socket + +import ( + "bytes" + "fmt" + "github.com/kisielk/og-rek" + "github.com/nlpodyssey/gopickle/pickle" + "net" +) + +const ( + commandTerminator = "" + pingCommand = "ping" + socketReadBufferSize = 10000 +) + +func (s *Fail2BanSocket) sendCommand(command []string) (interface{}, error) { + err := write(s.encoder, command) + if err != nil { + return nil, err + } + return read(&s.socket) +} + +func write(encoder *ogórek.Encoder, command []string) error { + err := encoder.Encode(command) + if err != nil { + return err + } + err = encoder.Encode(commandTerminator) + if err != nil { + return err + } + return nil +} + +func read(s *net.Conn) (interface{}, error) { + buf := make([]byte, socketReadBufferSize) + _, err := (*s).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() +} From 556c09c2f4c818240bea62b2feae076d62daf243 Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 11:26:40 +0100 Subject: [PATCH 3/7] fix handling of ping response Update the code handling sending the ping command to the fail2ban server to correctly handle the response. The response is of type `Tupple`, not `[]string`. --- src/socket/fail2banSocket.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go index 3df6d3c..907e5b9 100644 --- a/src/socket/fail2banSocket.go +++ b/src/socket/fail2banSocket.go @@ -1,9 +1,11 @@ package socket import ( - "github.com/kisielk/og-rek" "log" "net" + + "github.com/kisielk/og-rek" + "github.com/nlpodyssey/gopickle/types" ) type Fail2BanSocket struct { @@ -18,9 +20,7 @@ func MustConnectToSocket(path string) *Fail2BanSocket { } return &Fail2BanSocket{ socket: c, - encoder: ogórek.NewEncoderWithConfig(c, &ogórek.EncoderConfig{ - Protocol: 5, - }), + encoder: ogórek.NewEncoder(c), } } @@ -30,9 +30,13 @@ func (s *Fail2BanSocket) Ping() bool { log.Printf("server ping failed: %v", err) return false } - if v, ok := response.([]string); ok && v[1] == "pong" { - return true + + if t, ok := response.(*types.Tuple); ok { + if (*t)[1] == "pong" { + return true + } + log.Printf("unexpected response data: %s", t) } - log.Printf("unexpected response from server: %v", response) + log.Printf("unexpected response format - cannot parse: %v", response) return false } From a816558d491c0008d9d0f84d1dcb6e2ae899e778 Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 11:28:07 +0100 Subject: [PATCH 4/7] new metric for response of server ping Add a new metric to report back the response of the `ping` command. --- src/exporter.go | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/exporter.go b/src/exporter.go index 6cc3ad1..c29f343 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,10 +49,16 @@ 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 } @@ -58,6 +69,7 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- metricBannedIpsPerJail ch <- metricEnabledJails ch <- metricErrorCount + ch <- metricServerPing } func (e *Exporter) Collect(ch chan<- prometheus.Metric) { @@ -66,6 +78,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.collectEnabledJailMetrics(ch) e.collectUpMetric(ch) e.collectErrorCountMetric(ch) + e.collectServerPingMetric(ch) } func (e *Exporter) collectUpMetric(ch chan<- prometheus.Metric) { @@ -132,6 +145,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) @@ -146,6 +170,7 @@ func main() { exporter := &Exporter{ db: fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath), + socket: socket.MustConnectToSocket(appSettings.Fail2BanSocketPath), } prometheus.MustRegister(exporter) From 58694047c6913612aafd953383a558798edd245b Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 11:29:06 +0100 Subject: [PATCH 5/7] run go fmt --- src/cfg/cfg.go | 6 +++--- src/exporter.go | 2 +- src/socket/fail2banSocket.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cfg/cfg.go b/src/cfg/cfg.go index 1321fbb..0731fdb 100644 --- a/src/cfg/cfg.go +++ b/src/cfg/cfg.go @@ -12,9 +12,9 @@ const ( ) type AppSettings struct { - VersionMode bool - MetricsPort int - Fail2BanDbPath string + VersionMode bool + MetricsPort int + Fail2BanDbPath string Fail2BanSocketPath string } diff --git a/src/exporter.go b/src/exporter.go index c29f343..93cdec5 100644 --- a/src/exporter.go +++ b/src/exporter.go @@ -169,7 +169,7 @@ func main() { log.Print("starting fail2ban exporter") exporter := &Exporter{ - db: fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath), + db: fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath), socket: socket.MustConnectToSocket(appSettings.Fail2BanSocketPath), } prometheus.MustRegister(exporter) diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go index 907e5b9..35513c8 100644 --- a/src/socket/fail2banSocket.go +++ b/src/socket/fail2banSocket.go @@ -19,7 +19,7 @@ func MustConnectToSocket(path string) *Fail2BanSocket { log.Fatalf("failed to open fail2ban socket: %v", err) } return &Fail2BanSocket{ - socket: c, + socket: c, encoder: ogórek.NewEncoder(c), } } From 172971a055c3226a1ea9b2cf194c7d13b5d6ac7a Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 12:03:44 +0100 Subject: [PATCH 6/7] fix command writing code Fix the code writing commands to the fail2ban socket to correctly encode the command terminator string. The terminator string should be encoded as a simple byte array, not using the pickle format. Using the pickle format allowed the first command to succeed, but would "break" the socket in the sense that all following commands would fail with an underflow exception. --- src/socket/protocol.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/socket/protocol.go b/src/socket/protocol.go index 905cae1..9dc10f5 100644 --- a/src/socket/protocol.go +++ b/src/socket/protocol.go @@ -3,9 +3,7 @@ package socket import ( "bytes" "fmt" - "github.com/kisielk/og-rek" "github.com/nlpodyssey/gopickle/pickle" - "net" ) const ( @@ -15,28 +13,28 @@ const ( ) func (s *Fail2BanSocket) sendCommand(command []string) (interface{}, error) { - err := write(s.encoder, command) + err := s.write(command) if err != nil { return nil, err } - return read(&s.socket) + return s.read() } -func write(encoder *ogórek.Encoder, command []string) error { - err := encoder.Encode(command) +func (s *Fail2BanSocket) write(command []string) error { + err := s.encoder.Encode(command) if err != nil { return err } - err = encoder.Encode(commandTerminator) + _, err = s.socket.Write([]byte(commandTerminator)) if err != nil { return err } return nil } -func read(s *net.Conn) (interface{}, error) { +func (s *Fail2BanSocket) read() (interface{}, error) { buf := make([]byte, socketReadBufferSize) - _, err := (*s).Read(buf) + _, err := s.socket.Read(buf) if err != nil { return nil, err } From ef740512ca31c521e31b632b9840933717643f22 Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 12:21:44 +0100 Subject: [PATCH 7/7] db and socket paths are now optional When running the exporter at least one path is required (db, or socket), but not both. When one of the paths is not provided, the metrics that correspond to that source are not exporter. For example, if the database path is not set, the database metrics will not be shown. At least one path is required. Startup will fail if neither path provided. --- src/exporter.go | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/exporter.go b/src/exporter.go index 93cdec5..5d568c3 100644 --- a/src/exporter.go +++ b/src/exporter.go @@ -64,21 +64,29 @@ type Exporter struct { } func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - ch <- metricUp - ch <- metricBadIpsPerJail - ch <- metricBannedIpsPerJail - ch <- metricEnabledJails - ch <- metricErrorCount - ch <- metricServerPing + 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) - e.collectServerPingMetric(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) { @@ -168,9 +176,12 @@ func main() { } else { log.Print("starting fail2ban exporter") - exporter := &Exporter{ - db: fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath), - socket: socket.MustConnectToSocket(appSettings.Fail2BanSocketPath), + exporter := &Exporter{} + if appSettings.Fail2BanDbPath != "" { + exporter.db = fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath) + } + if appSettings.Fail2BanSocketPath != "" { + exporter.socket = socket.MustConnectToSocket(appSettings.Fail2BanSocketPath) } prometheus.MustRegister(exporter)