Reload guard.yaml without losing the bounties
systemctl reload guard, guard reload or SIGHUP reads the configuration again: a watch keeps its bounties and counts under the same name and takes its new settings at once; its source starts again only if it changed; watches gone stop, new ones start; a file with a mistake is refused. The Housecarl connection, state, socket and log wait for a restart, and guard reload says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c655abae2
commit
ecf4129877
11 files changed
+607
-93
No files matched your search
@@ -177,3 +177,140 @@ socket: ` + filepath.Join(dir, "guard.sock") + `
|
||||
t.Errorf("saved state: %v %+v", err, st2.Offences)
|
||||
}
|
||||
}
|
||||
|
||||
// stoppable is a chanSource that says when it stopped.
|
||||
type stoppable struct {
|
||||
chanSource
|
||||
stopped chan struct{}
|
||||
}
|
||||
|
||||
func (s *stoppable) Run(ctx context.Context, out chan<- source.Line) error {
|
||||
err := s.chanSource.Run(ctx, out)
|
||||
close(s.stopped)
|
||||
return err
|
||||
}
|
||||
|
||||
// TestReload: a reload keeps the bounties of a watch under the same name
|
||||
// and applies its new settings; a watch whose source changed gets a new
|
||||
// one; watches gone stop, new ones start; settings for a restart are
|
||||
// reported; a configuration with nothing to watch is refused.
|
||||
func TestReload(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
base := `housecarl: {url: https://fw:8070, list: web}
|
||||
defaults: {hits: 3, within: 10m, ban: 1h}
|
||||
state: ` + filepath.Join(dir, "state.json") + `
|
||||
socket: ` + filepath.Join(dir, "guard.sock") + `
|
||||
`
|
||||
parse := func(extra string) *config.Config {
|
||||
t.Helper()
|
||||
cfg, err := config.Parse([]byte(base + extra))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
cfg := parse(`watch:
|
||||
- {name: ssh, preset: sshd, file: /var/log/auth.log}
|
||||
- {name: web, file: /var/log/web.log, match: ['bad <IP>$']}
|
||||
`)
|
||||
st, _ := state.Load(cfg.State)
|
||||
hc := &fakeHC{}
|
||||
d, err := New(cfg, hc, nil, st, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var mu sync.Mutex
|
||||
srcs := map[string]*stoppable{}
|
||||
d.Sources = func(w *config.Watch) source.Source {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
s := &stoppable{chanSource{make(chan source.Line)}, make(chan struct{})}
|
||||
srcs[w.Name+" "+w.File] = s
|
||||
return s
|
||||
}
|
||||
src := func(key string) *stoppable {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return srcs[key]
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() { d.Run(ctx); close(done) }()
|
||||
waitFor(t, "the sources", func() bool { return src("ssh /var/log/auth.log") != nil && src("web /var/log/web.log") != nil })
|
||||
fail := func(s *stoppable, addr string, n int) {
|
||||
for i := 0; i < n; i++ {
|
||||
s.ch <- source.Line{Text: "Failed password for root from " + addr + " port 4000 ssh2", Time: time.Now()}
|
||||
}
|
||||
}
|
||||
ssh := src("ssh /var/log/auth.log")
|
||||
fail(ssh, "203.0.113.9", 2)
|
||||
|
||||
res, err := d.Reload(parse(`never_ban: [198.51.100.0/24]
|
||||
watch:
|
||||
- {name: ssh, preset: sshd, file: /var/log/auth.log, ban: {time: 2h}}
|
||||
- {name: mail, preset: dovecot, file: /var/log/mail.log}
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := strings.Join(res.Changes, "; "); got != "never_ban; watch ssh: ban; watch mail: added; watch web: removed" || len(res.Restart) != 0 {
|
||||
t.Errorf("reload: %q, restart %v", got, res.Restart)
|
||||
}
|
||||
select {
|
||||
case <-src("web /var/log/web.log").stopped:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("the removed watch's source still runs")
|
||||
}
|
||||
if src("mail /var/log/mail.log") == nil {
|
||||
t.Error("the added watch has no source")
|
||||
}
|
||||
// The bounty of 2 was kept: one more bans, for the new time.
|
||||
fail(ssh, "203.0.113.9", 1)
|
||||
waitFor(t, "the ban after the reload", func() bool { return hc.count() == 1 })
|
||||
if b := hc.last(); b.Time < 7190 || b.Time > 7200 {
|
||||
t.Errorf("ban after reload: %ds, want 2h", b.Time)
|
||||
}
|
||||
fail(ssh, "198.51.100.7", 3)
|
||||
|
||||
// A new file: a new source; the bounty stays; the hits change.
|
||||
fail(ssh, "203.0.113.50", 2)
|
||||
res, err = d.Reload(parse(`log: {level: debug}
|
||||
watch:
|
||||
- {name: ssh, preset: sshd, file: /var/log/secure, hits: 4, ban: {time: 2h}}
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := strings.Join(res.Changes, "; "); got != "never_ban; watch ssh: bounty 4 in 10m, source (started again); watch mail: removed" ||
|
||||
strings.Join(res.Restart, ",") != "log" {
|
||||
t.Errorf("reload: %q, restart %v", got, res.Restart)
|
||||
}
|
||||
select {
|
||||
case <-ssh.stopped:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("the old source still runs")
|
||||
}
|
||||
secure := src("ssh /var/log/secure")
|
||||
fail(secure, "203.0.113.50", 1)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if hc.count() != 1 {
|
||||
t.Fatal("banned below the new hits")
|
||||
}
|
||||
fail(secure, "203.0.113.50", 1)
|
||||
waitFor(t, "the ban at 4", func() bool { return hc.count() == 2 && hc.last().Address == "203.0.113.50" })
|
||||
if d.config().Log.Level != "info" {
|
||||
t.Error("the log level changed without a restart")
|
||||
}
|
||||
|
||||
// Nothing to watch: refused, the old configuration stays.
|
||||
if _, err := d.Reload(parse(`watch:
|
||||
- {name: ssh, preset: sshd, disabled: true}
|
||||
`)); err == nil {
|
||||
t.Error("a configuration with every watch disabled was taken")
|
||||
}
|
||||
if s := d.Status(); len(s.Watches) != 1 || s.Watches[0].Hits != 4 || s.Watches[0].Bans != 2 || s.Watches[0].Lines != 10 {
|
||||
t.Errorf("status after reloads %+v", s.Watches)
|
||||
}
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
Reference in new issue
Block a user