From 56730c8774906798aac2dc13393b471f51e6cfca Mon Sep 17 00:00:00 2001 From: Hector Date: Thu, 14 Oct 2021 20:52:25 +0000 Subject: [PATCH] feat: add new jail config metrics Add new metrics around basic jail configuration. The new metrics expose the max retries, ban time, and find time for each jail. Update project README with the new metrics. --- README.md | 15 ++++++++++++ src/collector/f2b/socket.go | 46 ++++++++++++++++++++++++++++++++++++ src/socket/fail2banSocket.go | 31 ++++++++++++++++++++++++ src/socket/protocol.go | 3 +++ 4 files changed, 95 insertions(+) diff --git a/README.md b/README.md index 85a13f3..ac10265 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,9 @@ Exposed metrics: * `jail_banned_total` (per jail) - Total number of banned IPs since fail2ban startup (includes expired bans) * `jail_failed_current` (per jail) - Number of current failures * `jail_failed_total` (per jail) - Total number of failures since fail2ban startup +* `jail_config_ban_time` (per jail) - How long an IP is banned for in this jail (in seconds) +* `jail_config_find_time` (per jail) - How far back the filter will look for failures in this jail (in seconds) +* `jail_config_max_retry` (per jail) - The max number of failures allowed before banning an IP in this jail * `version` - Version string of the exporter and fail2ban **Sample** @@ -186,6 +189,18 @@ f2b_jail_failed_current{jail="sshd"} 6 # TYPE f2b_jail_failed_total gauge f2b_jail_failed_total{jail="recidive"} 7 f2b_jail_failed_total{jail="sshd"} 125 +# HELP f2b_config_jail_ban_time How long an IP is banned for in this jail (in seconds) +# TYPE f2b_config_jail_ban_time gauge +f2b_config_jail_ban_time{jail="recidive"} 604800 +f2b_config_jail_ban_time{jail="sshd"} 600 +# HELP f2b_config_jail_find_time How far back will the filter look for failures in this jail (in seconds) +# TYPE f2b_config_jail_find_time gauge +f2b_config_jail_find_time{jail="recidive"} 86400 +f2b_config_jail_find_time{jail="sshd"} 600 +# HELP f2b_config_jail_max_retries The number of failures allowed until the IP is banned by this jail +# TYPE f2b_config_jail_max_retries gauge +f2b_config_jail_max_retries{jail="recidive"} 5 +f2b_config_jail_max_retries{jail="sshd"} 5 # HELP f2b_up Check if the fail2ban server is up # TYPE f2b_up gauge f2b_up 1 diff --git a/src/collector/f2b/socket.go b/src/collector/f2b/socket.go index 96cdd92..7dadd95 100644 --- a/src/collector/f2b/socket.go +++ b/src/collector/f2b/socket.go @@ -46,6 +46,21 @@ var ( "Total number of IPs banned by this jail (includes expired bans)", []string{"jail"}, nil, ) + metricJailBanTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "config", "jail_ban_time"), + "How long an IP is banned for in this jail (in seconds)", + []string{"jail"}, nil, + ) + metricJailFindTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "config", "jail_find_time"), + "How far back will the filter look for failures in this jail (in seconds)", + []string{"jail"}, nil, + ) + metricJailMaxRetry = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "config", "jail_max_retries"), + "The number of failures allowed until the IP is banned by this jail", + []string{"jail"}, nil, + ) metricVersionInfo = prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "version"), "Version of the exporter and fail2ban server", @@ -98,6 +113,7 @@ func (c *Collector) collectJailMetrics(ch chan<- prometheus.Metric, s *socket.Fa for i := range jails { c.collectJailStatsMetric(ch, s, jails[i]) + c.collectJailConfigMetrics(ch, s, jails[i]) } } @@ -123,6 +139,36 @@ func (c *Collector) collectJailStatsMetric(ch chan<- prometheus.Metric, s *socke ) } +func (c *Collector) collectJailConfigMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) { + banTime, err := s.GetJailBanTime(jail) + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get ban time for jail %s: %v", jail, err) + } else { + ch <- prometheus.MustNewConstMetric( + metricJailBanTime, prometheus.GaugeValue, float64(banTime), jail, + ) + } + findTime, err := s.GetJailFindTime(jail) + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get find time for jail %s: %v", jail, err) + } else { + ch <- prometheus.MustNewConstMetric( + metricJailFindTime, prometheus.GaugeValue, float64(findTime), jail, + ) + } + maxRetry, err := s.GetJailMaxRetries(jail) + if err != nil { + c.socketRequestErrorCount++ + log.Printf("failed to get max retries for jail %s: %v", jail, err) + } else { + ch <- prometheus.MustNewConstMetric( + metricJailMaxRetry, prometheus.GaugeValue, float64(maxRetry), jail, + ) + } +} + func (c *Collector) collectVersionMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) { var err error var fail2banVersion = "" diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go index 78133e3..0288dd9 100644 --- a/src/socket/fail2banSocket.go +++ b/src/socket/fail2banSocket.go @@ -118,6 +118,21 @@ func (s *Fail2BanSocket) GetJailStats(jail string) (JailStats, error) { return stats, newBadFormatError(statusCommand, response) } +func (s *Fail2BanSocket) GetJailBanTime(jail string) (int, error) { + command := fmt.Sprintf(banTimeCommandFmt, jail) + return s.sendSimpleIntCommand(command) +} + +func (s *Fail2BanSocket) GetJailFindTime(jail string) (int, error) { + command := fmt.Sprintf(findTimeCommandFmt, jail) + return s.sendSimpleIntCommand(command) +} + +func (s *Fail2BanSocket) GetJailMaxRetries(jail string) (int, error) { + command := fmt.Sprintf(maxRetriesCommandFmt, jail) + return s.sendSimpleIntCommand(command) +} + func (s *Fail2BanSocket) GetServerVersion() (string, error) { response, err := s.sendCommand([]string{versionCommand}) if err != nil { @@ -132,6 +147,22 @@ func (s *Fail2BanSocket) GetServerVersion() (string, error) { return "", newBadFormatError(versionCommand, response) } +// sendSimpleIntCommand sends a command to the fail2ban socket and parses the response to extract an int. +// This command assumes that the response data is in the format of `(d, d)` where `d` is a number. +func (s *Fail2BanSocket) sendSimpleIntCommand(command string) (int, error) { + response, err := s.sendCommand(strings.Split(command, " ")) + if err != nil { + return -1, err + } + + if lvl1, ok := response.(*types.Tuple); ok { + if banTime, ok := lvl1.Get(1).(int); ok { + return banTime, nil + } + } + return -1, newBadFormatError(command, response) +} + func newBadFormatError(command string, data interface{}) error { return fmt.Errorf("(%s) unexpected response format - cannot parse: %v", command, data) } diff --git a/src/socket/protocol.go b/src/socket/protocol.go index d510af7..54d3b6e 100644 --- a/src/socket/protocol.go +++ b/src/socket/protocol.go @@ -12,6 +12,9 @@ const ( pingCommand = "ping" statusCommand = "status" versionCommand = "version" + banTimeCommandFmt = "get %s bantime" + findTimeCommandFmt = "get %s findtime" + maxRetriesCommandFmt = "get %s maxretry" socketReadBufferSize = 1024 )