-
Notifications
You must be signed in to change notification settings - Fork 3
/
banlist.go
206 lines (184 loc) · 5.36 KB
/
banlist.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package caddy_fail2ban
import (
"bufio"
"fmt"
"os"
"path/filepath"
"github.com/caddyserver/caddy/v2"
"github.com/fsnotify/fsnotify"
"go.uber.org/zap"
)
type banQuery struct {
response chan bool
ip string
}
type Banlist struct {
ctx caddy.Context
bannedIps []string
shutdown chan bool
queries chan banQuery
logger *zap.Logger
banfile *string
reload chan chan bool
reloadSubs []chan bool
}
func NewBanlist(ctx caddy.Context, logger *zap.Logger, banfile *string) Banlist {
banlist := Banlist{
ctx: ctx,
queries: make(chan banQuery),
logger: logger,
banfile: banfile,
reload: make(chan chan bool),
}
return banlist
}
func (b *Banlist) Start() {
go b.monitorBannedIps()
}
func (b *Banlist) IsBanned(remote_ip string) bool {
response := make(chan bool)
query := banQuery{
response,
remote_ip,
}
b.queries <- query
isBanned := <-response
close(response)
return isBanned
}
func (b *Banlist) Reload() {
resp := make(chan bool)
b.reload <- resp
<-resp
}
func (b *Banlist) monitorBannedIps() {
b.logger.Info("Starting monitor for banned IPs")
defer func() {
b.logger.Info("Shutting down monitor for banned IPs")
}()
// Load initial list
err := b.loadBannedIps()
if err != nil {
b.logger.Error("Error loading initial list of banned IPs", zap.Error(err))
return
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
b.logger.Error("Error creating monitor", zap.Error(err))
return
}
defer watcher.Close()
// Watch the directory that the banfile is in as sometimes files can be
// written to by replacement (see https://pkg.go.dev/github.com/fsnotify/fsnotify#readme-watching-a-file-doesn-t-work-well)
err = watcher.Add(filepath.Dir(*b.banfile))
if err != nil {
b.logger.Error("Error monitoring banfile", zap.Error(err), zap.String("banfile", *b.banfile))
}
for {
select {
case resp := <-b.reload:
// Trigger reload of banned IPs
err = b.loadBannedIps()
if err != nil {
b.logger.Error("Error when trying to explicitly reloading list of banned IPs", zap.Error(err))
return
}
b.logger.Debug("Banlist reloaded")
resp <- true
case query := <-b.queries:
// Respond to query whether an IP has been banned
b.logger.Debug("Handling ban query", zap.String("remote_ip", query.ip))
b.handleQuery(query)
case err, ok := <-watcher.Errors:
// Handle errors from fsnotify
if !ok {
b.logger.Error("Error channel closed unexpectedly, stopping monitor")
return
}
b.logger.Error("Error from fsnotify", zap.Error(err))
case event, ok := <-watcher.Events:
// Respond to changed file events from fsnotify
if !ok {
b.logger.Error("Watcher closed unexpectedly, stopping monitor")
return
}
// We get events for the whole directory but only want to do work if the
// changed file is our banfile
if (event.Has(fsnotify.Write) || event.Has(fsnotify.Create)) && event.Name == *b.banfile {
b.logger.Debug("File has changed, reloading banned IPs")
err = b.loadBannedIps()
if err != nil {
b.logger.Error("Error when trying to reload banned IPs because of inotify event", zap.Error(err))
return
}
}
case <-b.ctx.Done():
// Caddy will close the context when it's time to shut down
b.logger.Debug("Context finished, shutting down")
return
}
}
}
func (b *Banlist) handleQuery(query banQuery) {
remote_ip := query.ip
for _, ip := range b.bannedIps {
b.logger.Debug("Checking IP", zap.String("ip", ip), zap.String("remote_ip", remote_ip))
if ip == remote_ip {
query.response <- true
return
}
}
query.response <- false
}
// Provide a channel that will receive a boolean true value whenever the list
// of banned IPs has been reloaded. Mostly useful for tests so they can wait
// for the inotify event rather than sleep
func (b *Banlist) subscribeToReload(notify chan bool) {
b.reloadSubs = append(b.reloadSubs, notify)
}
// loadBannedIps loads list of banned IPs from file on disk and notifies
// subscribers in case it was successful
func (b *Banlist) loadBannedIps() error {
bannedIps, err := b.getBannedIps()
if err != nil {
b.logger.Error("Error getting list of banned IPs")
return err
} else {
b.bannedIps = bannedIps
for _, n := range b.reloadSubs {
n <- true
}
// only respond once then clear subs, otherwise further attempts might
// block as the receiver only reads one event rather than constantly
// draining it.
b.reloadSubs = nil
return nil
}
}
func (b *Banlist) getBannedIps() ([]string, error) {
// Open banfile
// Try to open file
banfileHandle, err := os.Open(*b.banfile)
if err != nil {
b.logger.Info("Creating new file since Open failed", zap.String("banfile", *b.banfile), zap.Error(err))
// Try to create new file, maybe the file didn't exist yet
banfileHandle, err = os.Create(*b.banfile)
if err != nil {
b.logger.Error("Error creating banfile", zap.String("banfile", *b.banfile), zap.Error(err))
return nil, fmt.Errorf("cannot open or create banfile: %v", err)
}
}
defer banfileHandle.Close()
// read banned IPs
bannedIps := make([]string, 0)
scanner := bufio.NewScanner(banfileHandle)
for scanner.Scan() {
line := scanner.Text()
b.logger.Debug("Adding banned IP to list", zap.String("banned_addr", line))
bannedIps = append(bannedIps, line)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error parsing banfile: %v", err)
}
return bannedIps, nil
}