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() +}