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.
This commit is contained in:
Hector 2021-08-29 11:50:53 +00:00
parent 9d6b35c59a
commit 39133d0a76
7 changed files with 167 additions and 20 deletions

View file

@ -12,9 +12,10 @@ const (
) )
type AppSettings struct { type AppSettings struct {
VersionMode bool VersionMode bool
MetricsPort int MetricsPort int
Fail2BanDbPath string Fail2BanDbPath string
Fail2BanSocketPath string
} }
func Parse() *AppSettings { func Parse() *AppSettings {
@ -22,6 +23,7 @@ func Parse() *AppSettings {
flag.BoolVar(&appSettings.VersionMode, "version", false, "show version info and exit") 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.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.Fail2BanDbPath, "db", "", "path to the fail2ban sqlite database")
flag.StringVar(&appSettings.Fail2BanSocketPath, "socket", "", "path to the fail2ban server socket")
flag.Parse() flag.Parse()
appSettings.validateFlags() appSettings.validateFlags()
@ -31,8 +33,8 @@ func Parse() *AppSettings {
func (settings *AppSettings) validateFlags() { func (settings *AppSettings) validateFlags() {
var flagsValid = true var flagsValid = true
if !settings.VersionMode { if !settings.VersionMode {
if settings.Fail2BanDbPath == "" { if settings.Fail2BanDbPath == "" && settings.Fail2BanSocketPath == "" {
fmt.Println("missing flag 'db'") fmt.Println("at least one of the following flags must be provided: 'db', 'socket'")
flagsValid = false flagsValid = false
} }
if settings.MetricsPort < minServerPort || settings.MetricsPort > maxServerPort { if settings.MetricsPort < minServerPort || settings.MetricsPort > maxServerPort {

View file

@ -3,15 +3,20 @@ package main
import ( import (
"fail2ban-prometheus-exporter/cfg" "fail2ban-prometheus-exporter/cfg"
fail2banDb "fail2ban-prometheus-exporter/db" fail2banDb "fail2ban-prometheus-exporter/db"
"fail2ban-prometheus-exporter/socket"
"fmt" "fmt"
"log"
"net/http"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
) )
const namespace = "fail2ban" const (
namespace = "fail2ban"
sockNamespace = "f2b"
)
var ( var (
version = "dev" version = "dev"
@ -44,28 +49,44 @@ var (
"Number of errors found since startup.", "Number of errors found since startup.",
[]string{"type"}, nil, []string{"type"}, nil,
) )
metricServerPing = prometheus.NewDesc(
prometheus.BuildFQName(sockNamespace, "", "up"),
"Check if the fail2ban server is up",
nil, nil,
)
) )
type Exporter struct { type Exporter struct {
db *fail2banDb.Fail2BanDB db *fail2banDb.Fail2BanDB
socket *socket.Fail2BanSocket
lastError error lastError error
dbErrorCount int dbErrorCount int
} }
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- metricUp if e.db != nil {
ch <- metricBadIpsPerJail ch <- metricUp
ch <- metricBannedIpsPerJail ch <- metricBadIpsPerJail
ch <- metricEnabledJails ch <- metricBannedIpsPerJail
ch <- metricErrorCount ch <- metricEnabledJails
ch <- metricErrorCount
}
if e.socket != nil {
ch <- metricServerPing
}
} }
func (e *Exporter) Collect(ch chan<- prometheus.Metric) { func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
e.collectBadIpsPerJailMetrics(ch) if e.db != nil {
e.collectBannedIpsPerJailMetrics(ch) e.collectBadIpsPerJailMetrics(ch)
e.collectEnabledJailMetrics(ch) e.collectBannedIpsPerJailMetrics(ch)
e.collectUpMetric(ch) e.collectEnabledJailMetrics(ch)
e.collectErrorCountMetric(ch) e.collectUpMetric(ch)
e.collectErrorCountMetric(ch)
}
if e.socket != nil {
e.collectServerPingMetric(ch)
}
} }
func (e *Exporter) collectUpMetric(ch chan<- prometheus.Metric) { 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() { func printAppVersion() {
fmt.Println(version) fmt.Println(version)
fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy) 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 { } else {
log.Print("starting fail2ban exporter") log.Print("starting fail2ban exporter")
exporter := &Exporter{ exporter := &Exporter{}
db: fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath), if appSettings.Fail2BanDbPath != "" {
exporter.db = fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath)
}
if appSettings.Fail2BanSocketPath != "" {
exporter.socket = socket.MustConnectToSocket(appSettings.Fail2BanSocketPath)
} }
prometheus.MustRegister(exporter) prometheus.MustRegister(exporter)

View file

@ -3,6 +3,8 @@ module fail2ban-prometheus-exporter
go 1.15 go 1.15
require ( require (
github.com/kisielk/og-rek v1.1.0
github.com/mattn/go-sqlite3 v1.14.6 github.com/mattn/go-sqlite3 v1.14.6
github.com/nlpodyssey/gopickle v0.1.0
github.com/prometheus/client_golang v1.9.0 github.com/prometheus/client_golang v1.9.0
) )

View file

@ -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/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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/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.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/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= 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.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/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/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/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/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= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=

8
src/socket/decoder.go Normal file
View file

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

View file

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

53
src/socket/protocol.go Normal file
View file

@ -0,0 +1,53 @@
package socket
import (
"bytes"
"fmt"
"github.com/nlpodyssey/gopickle/pickle"
)
const (
commandTerminator = "<F2B_END_COMMAND>"
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()
}