Guard: watch logs, ban misbehaving addresses in Housecarl
Files and the journal, patterns with one <IP>, bounties within a window, bans in a Housecarl ban list with repeat offenders' bans growing; presets for sshd, jarl, housecarl, steward, bind-xfr, postfix-sasl, dovecot, nginx-auth and housecarl-drops; guard run/check/test/status/bans/ban/unban; a Debian package with a sandboxed unit; an end-to-end test with Housecarl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d06bf8d531
47 files changed
+4614
No files matched your search
@@ -0,0 +1,89 @@
|
||||
// Package match finds the address in a log line: patterns are regular
|
||||
// expressions (Go's, RE2) with one <IP>, which matches an IPv4 or IPv6
|
||||
// address (bracketed, IPv4-mapped, with a zone). The address is checked
|
||||
// with netip, so what only looks like one (a time, 12:34:56) never is.
|
||||
package match
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Placeholder is where a pattern's address is.
|
||||
const Placeholder = "<IP>"
|
||||
|
||||
// ipRe matches an address: IPv4, or IPv6 (which may end in IPv4,
|
||||
// ::ffff:203.0.113.9), bracketed or not, with an optional zone.
|
||||
const ipRe = `\[?(?P<ip>[0-9]{1,3}(?:\.[0-9]{1,3}){3}|[0-9A-Fa-f]{0,4}(?::[0-9A-Fa-f]{0,4}){1,7}(?:[0-9]{1,3}(?:\.[0-9]{1,3}){3})?(?:%[0-9A-Za-z_.-]+)?)\]?`
|
||||
|
||||
// Matcher holds a watch's patterns.
|
||||
type Matcher struct {
|
||||
match []*regexp.Regexp
|
||||
ignore []*regexp.Regexp
|
||||
}
|
||||
|
||||
// Compile makes a Matcher. Each match pattern must hold <IP> exactly
|
||||
// once; ignore patterns are plain regular expressions.
|
||||
func Compile(match, ignore []string) (*Matcher, error) {
|
||||
m := &Matcher{}
|
||||
for _, p := range match {
|
||||
if n := strings.Count(p, Placeholder); n != 1 {
|
||||
return nil, fmt.Errorf("pattern %q has %d %s; it needs exactly one, where the address is", p, n, Placeholder)
|
||||
}
|
||||
re, err := regexp.Compile(strings.Replace(p, Placeholder, ipRe, 1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pattern %q: %v", p, err)
|
||||
}
|
||||
m.match = append(m.match, re)
|
||||
}
|
||||
for _, p := range ignore {
|
||||
re, err := regexp.Compile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ignore pattern %q: %v", p, err)
|
||||
}
|
||||
m.ignore = append(m.ignore, re)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Match returns the address of the first pattern a line matches, unless
|
||||
// an ignore pattern matches it too.
|
||||
func (m *Matcher) Match(line string) (netip.Addr, bool) {
|
||||
for _, re := range m.match {
|
||||
sub := re.FindStringSubmatch(line)
|
||||
if sub == nil {
|
||||
continue
|
||||
}
|
||||
text := sub[re.SubexpIndex("ip")]
|
||||
if i := strings.IndexByte(text, '%'); i >= 0 {
|
||||
text = text[:i]
|
||||
}
|
||||
a, err := netip.ParseAddr(text)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, ig := range m.ignore {
|
||||
if ig.MatchString(line) {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
}
|
||||
return a.Unmap(), true
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// Group is the prefix an address is counted and banned by: an IPv4
|
||||
// address alone, an IPv6 address by its /v6 (64: what one customer
|
||||
// usually has).
|
||||
func Group(a netip.Addr, v6 int) netip.Prefix {
|
||||
if a.Is4() || v6 <= 0 || v6 > 128 {
|
||||
if a.Is4() {
|
||||
return netip.PrefixFrom(a, 32)
|
||||
}
|
||||
v6 = 128
|
||||
}
|
||||
p, _ := a.Prefix(v6)
|
||||
return p
|
||||
}
|
||||
Reference in new issue
Block a user