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>
90 lines
2.5 KiB
Go
90 lines
2.5 KiB
Go
// 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
|
|
}
|