master
go 165 lines 3.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 //go:build linux
4
5 package fail2ban
6
7 import (
8 "bufio"
9 "bytes"
10 "errors"
11 "fmt"
12 "strconv"
13 "strings"
14 "time"
15 )
16
17 func (c *Collector) collect() (map[string]int64, error) {
18 now := time.Now()
19
20 if now.Sub(c.lastDiscoverTime) > c.discoverEvery || c.forceDiscover {
21 jails, err := c.discoverJails()
22 if err != nil {
23 return nil, err
24 }
25 c.jails = jails
26 c.lastDiscoverTime = now
27 c.forceDiscover = false
28 }
29
30 mx := make(map[string]int64)
31
32 if err := c.collectJails(mx); err != nil {
33 return nil, err
34 }
35
36 return mx, nil
37 }
38
39 func (c *Collector) discoverJails() ([]string, error) {
40 bs, err := c.exec.status()
41 if err != nil {
42 return nil, err
43 }
44
45 jails, err := parseFail2banStatus(bs)
46 if err != nil {
47 return nil, err
48 }
49
50 if len(jails) == 0 {
51 return nil, errors.New("no jails found")
52 }
53
54 c.Debugf("discovered %d jails: %v", len(jails), jails)
55
56 return jails, nil
57 }
58
59 func (c *Collector) collectJails(mx map[string]int64) error {
60 seen := make(map[string]bool)
61
62 for _, jail := range c.jails {
63 c.Debugf("querying status for jail '%s'", jail)
64 bs, err := c.exec.jailStatus(jail)
65 if err != nil {
66 if errors.Is(err, errJailNotExist) {
67 c.forceDiscover = true
68 continue
69 }
70 return err
71 }
72
73 failed, banned, err := parseFail2banJailStatus(bs)
74 if err != nil {
75 return err
76 }
77
78 if !c.seenJails[jail] {
79 c.seenJails[jail] = true
80 c.addJailCharts(jail)
81 }
82 seen[jail] = true
83
84 px := fmt.Sprintf("jail_%s_", jail)
85
86 mx[px+"currently_failed"] = failed
87 mx[px+"currently_banned"] = banned
88 }
89
90 for jail := range c.seenJails {
91 if !seen[jail] {
92 delete(c.seenJails, jail)
93 c.removeJailCharts(jail)
94 }
95 }
96
97 return nil
98 }
99
100 func parseFail2banJailStatus(jailStatus []byte) (failed, banned int64, err error) {
101 const (
102 failedSub = "Currently failed:"
103 bannedSub = "Currently banned:"
104 )
105
106 var failedFound, bannedFound bool
107
108 sc := bufio.NewScanner(bytes.NewReader(jailStatus))
109
110 for sc.Scan() && !(failedFound && bannedFound) {
111 text := strings.TrimSpace(sc.Text())
112 if text == "" {
113 continue
114 }
115
116 if !failedFound {
117 if _, after, ok := strings.Cut(text, failedSub); ok {
118 failedFound = true
119 s := strings.TrimSpace(after)
120 if failed, err = strconv.ParseInt(s, 10, 64); err != nil {
121 return 0, 0, fmt.Errorf("failed to parse currently failed value (%s): %v", s, err)
122 }
123 }
124 }
125 if !bannedFound {
126 if _, after, ok := strings.Cut(text, bannedSub); ok {
127 bannedFound = true
128 s := strings.TrimSpace(after)
129 if banned, err = strconv.ParseInt(s, 10, 64); err != nil {
130 return 0, 0, fmt.Errorf("failed to parse currently banned value (%s): %v", s, err)
131 }
132 }
133 }
134 }
135
136 if !failedFound || !bannedFound {
137 return 0, 0, errors.New("failed to find failed and banned values")
138 }
139
140 return failed, banned, nil
141 }
142
143 func parseFail2banStatus(status []byte) ([]string, error) {
144 const sub = "Jail list:"
145
146 var jails []string
147
148 sc := bufio.NewScanner(bytes.NewReader(status))
149
150 for sc.Scan() {
151 text := strings.TrimSpace(sc.Text())
152
153 if _, after, ok := strings.Cut(text, sub); ok {
154 s := strings.ReplaceAll(after, ",", "")
155 jails = strings.Fields(s)
156 break
157 }
158 }
159
160 if len(jails) == 0 {
161 return nil, errors.New("failed to find jails")
162 }
163
164 return jails, nil
165 }