From bd6566eea853c1203ccb04cee1b3b5040d5ace36 Mon Sep 17 00:00:00 2001
From: Hector <dev@hsmith.org>
Date: Sun, 29 Aug 2021 17:05:48 +0100
Subject: [PATCH 1/3] new status command

Add support for running the `status` command and processing the server
response data.
Add new metric for the number of jails.
---
 src/exporter.go              | 18 ++++++++++++++++++
 src/socket/fail2banSocket.go | 35 +++++++++++++++++++++++++++++++++--
 src/socket/protocol.go       |  1 +
 3 files changed, 52 insertions(+), 2 deletions(-)

diff --git a/src/exporter.go b/src/exporter.go
index 5d568c3..6dd422f 100644
--- a/src/exporter.go
+++ b/src/exporter.go
@@ -54,6 +54,11 @@ var (
 		"Check if the fail2ban server is up",
 		nil, nil,
 	)
+	metricJailCount = prometheus.NewDesc(
+		prometheus.BuildFQName(sockNamespace, "", "jail_count"),
+		"Number of defined jails",
+		nil, nil,
+	)
 )
 
 type Exporter struct {
@@ -73,6 +78,7 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
 	}
 	if e.socket != nil {
 		ch <- metricServerPing
+		ch <- metricJailCount
 	}
 }
 
@@ -86,6 +92,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
 	}
 	if e.socket != nil {
 		e.collectServerPingMetric(ch)
+		e.collectJailCountMetric(ch)
 	}
 }
 
@@ -164,6 +171,17 @@ func (e *Exporter) collectServerPingMetric(ch chan<- prometheus.Metric) {
 	)
 }
 
+func (e *Exporter) collectJailCountMetric(ch chan<- prometheus.Metric) {
+	jails, err := e.socket.GetJails()
+	var count float64 = 0
+	if err == nil {
+		count = float64(len(jails))
+	}
+	ch <- prometheus.MustNewConstMetric(
+		metricJailCount, prometheus.GaugeValue, count,
+	)
+}
+
 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)
diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go
index 35513c8..48e2ad6 100644
--- a/src/socket/fail2banSocket.go
+++ b/src/socket/fail2banSocket.go
@@ -3,7 +3,8 @@ package socket
 import (
 	"log"
 	"net"
-
+	"fmt"
+	"strings"
 	"github.com/kisielk/og-rek"
 	"github.com/nlpodyssey/gopickle/types"
 )
@@ -37,6 +38,36 @@ func (s *Fail2BanSocket) Ping() bool {
 		}
 		log.Printf("unexpected response data: %s", t)
 	}
-	log.Printf("unexpected response format - cannot parse: %v", response)
+	log.Printf("(%s) unexpected response format - cannot parse: %v", pingCommand, response)
 	return false
 }
+
+func (s *Fail2BanSocket) GetJails() ([]string, error) {
+	response, err := s.sendCommand([]string{statusCommand})
+	if err != nil {
+		return nil, err
+	}
+
+	if lvl1, ok := response.(*types.Tuple); ok {
+		if lvl2, ok := lvl1.Get(1).(*types.List); ok {
+			if lvl3, ok := lvl2.Get(1).(*types.Tuple); ok {
+				if lvl4, ok := lvl3.Get(1).(string); ok {
+					splitJails := strings.Split(lvl4, ",")
+					return trimSpaceForAll(splitJails), nil
+				}
+			}
+		}
+	}
+	return nil, newBadFormatError(statusCommand, response)
+}
+
+func newBadFormatError(command string, data interface{}) error {
+	return fmt.Errorf("(%s) unexpected response format - cannot parse: %v", command, data)
+}
+
+func trimSpaceForAll(slice []string) []string {
+	for i := range slice {
+		slice[i] = strings.TrimSpace(slice[i])
+	}
+	return slice
+}
diff --git a/src/socket/protocol.go b/src/socket/protocol.go
index 1cf533d..5320f5b 100644
--- a/src/socket/protocol.go
+++ b/src/socket/protocol.go
@@ -10,6 +10,7 @@ import (
 const (
 	commandTerminator    = "<F2B_END_COMMAND>"
 	pingCommand          = "ping"
+	statusCommand        = "status"
 	socketReadBufferSize = 1024
 )
 

From 3dd68cd8e6372b4b6c919dbfe9320e9b5ac93876 Mon Sep 17 00:00:00 2001
From: Hector <dev@hsmith.org>
Date: Sun, 29 Aug 2021 17:33:12 +0100
Subject: [PATCH 2/3] collect metrics on jail stats

Collect more stats on each jail (number of current/total failures, and
number of current/total bans). This is the same data shown by the client
when running the `status` command.
Add new metrics to export the new data.
---
 src/exporter.go              | 54 ++++++++++++++++++++++++++++++++--
 src/socket/fail2banSocket.go | 56 ++++++++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+), 2 deletions(-)

diff --git a/src/exporter.go b/src/exporter.go
index 6dd422f..146eab4 100644
--- a/src/exporter.go
+++ b/src/exporter.go
@@ -49,6 +49,7 @@ var (
 		"Number of errors found since startup.",
 		[]string{"type"}, nil,
 	)
+	
 	metricServerPing = prometheus.NewDesc(
 		prometheus.BuildFQName(sockNamespace, "", "up"),
 		"Check if the fail2ban server is up",
@@ -59,6 +60,26 @@ var (
 		"Number of defined jails",
 		nil, nil,
 	)
+	metricJailFailedCurrent = prometheus.NewDesc(
+		prometheus.BuildFQName(sockNamespace, "", "jail_failed_current"),
+		"Number of current failures on this jail's filter",
+		[]string{"jail"}, nil,
+	)
+	metricJailFailedTotal = prometheus.NewDesc(
+		prometheus.BuildFQName(sockNamespace, "", "jail_failed_total"),
+		"Number of total failures on this jail's filter",
+		[]string{"jail"}, nil,
+	)
+	metricJailBannedCurrent = prometheus.NewDesc(
+		prometheus.BuildFQName(sockNamespace, "", "jail_banned_current"),
+		"Number of IPs currently banned in this jail",
+		[]string{"jail"}, nil,
+	)
+	metricJailBannedTotal = prometheus.NewDesc(
+		prometheus.BuildFQName(sockNamespace, "", "jail_banned_total"),
+		"Total number of IPs banned by this jail (includes expired bans)",
+		[]string{"jail"}, nil,
+	)
 )
 
 type Exporter struct {
@@ -79,6 +100,10 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
 	if e.socket != nil {
 		ch <- metricServerPing
 		ch <- metricJailCount
+		ch <- metricJailFailedCurrent
+		ch <- metricJailFailedTotal
+		ch <- metricJailBannedCurrent
+		ch <- metricJailBannedTotal
 	}
 }
 
@@ -92,7 +117,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
 	}
 	if e.socket != nil {
 		e.collectServerPingMetric(ch)
-		e.collectJailCountMetric(ch)
+		e.collectJailMetrics(ch)
 	}
 }
 
@@ -171,7 +196,7 @@ func (e *Exporter) collectServerPingMetric(ch chan<- prometheus.Metric) {
 	)
 }
 
-func (e *Exporter) collectJailCountMetric(ch chan<- prometheus.Metric) {
+func (e *Exporter) collectJailMetrics(ch chan<- prometheus.Metric) {
 	jails, err := e.socket.GetJails()
 	var count float64 = 0
 	if err == nil {
@@ -180,6 +205,31 @@ func (e *Exporter) collectJailCountMetric(ch chan<- prometheus.Metric) {
 	ch <- prometheus.MustNewConstMetric(
 		metricJailCount, prometheus.GaugeValue, count,
 	)
+
+	for i := range(jails) {
+		e.collectJailStatsMetric(ch, jails[i])
+	}
+}
+
+func (e *Exporter) collectJailStatsMetric(ch chan<- prometheus.Metric, jail string) {
+	stats, err := e.socket.GetJailStats(jail)
+	if err != nil {
+		log.Printf("failed to get stats for jail %s: %v", jail, err)
+		return
+	}
+
+	ch <- prometheus.MustNewConstMetric(
+		metricJailFailedCurrent, prometheus.GaugeValue, float64(stats.FailedCurrent), jail,
+	)
+	ch <- prometheus.MustNewConstMetric(
+		metricJailFailedTotal, prometheus.GaugeValue, float64(stats.FailedTotal), jail,
+	)
+	ch <- prometheus.MustNewConstMetric(
+		metricJailBannedCurrent, prometheus.GaugeValue, float64(stats.BannedCurrent), jail,
+	)
+	ch <- prometheus.MustNewConstMetric(
+		metricJailBannedTotal, prometheus.GaugeValue, float64(stats.BannedTotal), jail,
+	)
 }
 
 func printAppVersion() {
diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go
index 48e2ad6..61d273d 100644
--- a/src/socket/fail2banSocket.go
+++ b/src/socket/fail2banSocket.go
@@ -14,6 +14,13 @@ type Fail2BanSocket struct {
 	encoder *ogórek.Encoder
 }
 
+type JailStats struct {
+	FailedCurrent int
+	FailedTotal   int
+	BannedCurrent int
+	BannedTotal   int
+}
+
 func MustConnectToSocket(path string) *Fail2BanSocket {
 	c, err := net.Dial("unix", path)
 	if err != nil {
@@ -61,6 +68,55 @@ func (s *Fail2BanSocket) GetJails() ([]string, error) {
 	return nil, newBadFormatError(statusCommand, response)
 }
 
+func (s *Fail2BanSocket) GetJailStats(jail string) (JailStats, error) {
+	response, err := s.sendCommand([]string{statusCommand, jail})
+	if err != nil {
+		return JailStats{}, err
+	}
+
+	stats := JailStats{
+		FailedCurrent: -1,
+		FailedTotal: -1,
+		BannedCurrent: -1,
+		BannedTotal: -1,
+	}
+
+	if lvl1, ok := response.(*types.Tuple); ok {
+		if lvl2, ok := lvl1.Get(1).(*types.List); ok {
+			if filter, ok := lvl2.Get(0).(*types.Tuple); ok {
+				if filterLvl1, ok := filter.Get(1).(*types.List); ok {
+					if filterCurrentTuple, ok := filterLvl1.Get(0).(*types.Tuple); ok {
+						if filterCurrent, ok := filterCurrentTuple.Get(1).(int); ok {
+							stats.FailedCurrent = filterCurrent
+						}
+					}
+					if filterTotalTuple, ok := filterLvl1.Get(1).(*types.Tuple); ok {
+						if filterTotal, ok := filterTotalTuple.Get(1).(int); ok {
+							stats.FailedTotal = filterTotal
+						}
+					}
+				}
+			}
+			if actions, ok := lvl2.Get(1).(*types.Tuple); ok {
+				if actionsLvl1, ok := actions.Get(1).(*types.List); ok {
+					if actionsCurrentTuple, ok := actionsLvl1.Get(0).(*types.Tuple); ok {
+						if actionsCurrent, ok := actionsCurrentTuple.Get(1).(int); ok {
+							stats.BannedCurrent = actionsCurrent
+						}
+					}
+					if actionsTotalTuple, ok := actionsLvl1.Get(1).(*types.Tuple); ok {
+						if actionsTotal, ok := actionsTotalTuple.Get(1).(int); ok {
+							stats.BannedTotal = actionsTotal
+						}
+					}
+				}
+			}
+			return stats, nil
+		}
+	}
+	return stats, newBadFormatError(statusCommand, response)
+}
+
 func newBadFormatError(command string, data interface{}) error {
 	return fmt.Errorf("(%s) unexpected response format - cannot parse: %v", command, data)
 }

From efc789cec2ffe30545644c19f7c7a13b8c7d0dc7 Mon Sep 17 00:00:00 2001
From: Hector <dev@hsmith.org>
Date: Sun, 29 Aug 2021 17:33:48 +0100
Subject: [PATCH 3/3] go fmt

---
 src/exporter.go              |  4 ++--
 src/socket/fail2banSocket.go | 10 +++++-----
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/exporter.go b/src/exporter.go
index 146eab4..97e8887 100644
--- a/src/exporter.go
+++ b/src/exporter.go
@@ -49,7 +49,7 @@ var (
 		"Number of errors found since startup.",
 		[]string{"type"}, nil,
 	)
-	
+
 	metricServerPing = prometheus.NewDesc(
 		prometheus.BuildFQName(sockNamespace, "", "up"),
 		"Check if the fail2ban server is up",
@@ -206,7 +206,7 @@ func (e *Exporter) collectJailMetrics(ch chan<- prometheus.Metric) {
 		metricJailCount, prometheus.GaugeValue, count,
 	)
 
-	for i := range(jails) {
+	for i := range jails {
 		e.collectJailStatsMetric(ch, jails[i])
 	}
 }
diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go
index 61d273d..8cce14f 100644
--- a/src/socket/fail2banSocket.go
+++ b/src/socket/fail2banSocket.go
@@ -1,12 +1,12 @@
 package socket
 
 import (
-	"log"
-	"net"
 	"fmt"
-	"strings"
 	"github.com/kisielk/og-rek"
 	"github.com/nlpodyssey/gopickle/types"
+	"log"
+	"net"
+	"strings"
 )
 
 type Fail2BanSocket struct {
@@ -76,9 +76,9 @@ func (s *Fail2BanSocket) GetJailStats(jail string) (JailStats, error) {
 
 	stats := JailStats{
 		FailedCurrent: -1,
-		FailedTotal: -1,
+		FailedTotal:   -1,
 		BannedCurrent: -1,
-		BannedTotal: -1,
+		BannedTotal:   -1,
 	}
 
 	if lvl1, ok := response.(*types.Tuple); ok {