Feat: fail2ban whitelist (#546)
* feat: support whitelist in failtoban plugin * test: add e2e test for failtoban ignore ip * refactor: use netipx for easy contains check
This commit is contained in:
parent
486bd74534
commit
9ff3550786
4 changed files with 294 additions and 1 deletions
|
|
@ -240,3 +240,235 @@ func TestFailtobanPipeCreateFail(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailtobanIgnoreIP(t *testing.T) {
|
||||
piperaddr, piperport := nextAvailablePiperAddress()
|
||||
|
||||
piper, _, _, err := runCmd("/sshpiperd/sshpiperd",
|
||||
"-p",
|
||||
piperport,
|
||||
"/sshpiperd/plugins/fixed",
|
||||
"--target",
|
||||
"host-password:2222",
|
||||
"--",
|
||||
"/sshpiperd/plugins/failtoban",
|
||||
"--max-failures",
|
||||
"3",
|
||||
"--ignore-ip",
|
||||
"127.0.0.1",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to run sshpiperd: %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(piper)
|
||||
|
||||
waitForEndpointReady(piperaddr)
|
||||
|
||||
{
|
||||
randtext := uuid.New().String()
|
||||
targetfie := uuid.New().String()
|
||||
|
||||
c, stdin, stdout, err := runCmd(
|
||||
"ssh",
|
||||
"-v",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-p",
|
||||
piperport,
|
||||
"-l",
|
||||
"user",
|
||||
"127.0.0.1",
|
||||
fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to ssh to piper-workingdir, %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(c)
|
||||
|
||||
enterPassword(stdin, stdout, "pass")
|
||||
|
||||
time.Sleep(time.Second) // wait for file flush
|
||||
|
||||
checkSharedFileContent(t, targetfie, randtext)
|
||||
}
|
||||
|
||||
// run 3 times with wrong password
|
||||
{
|
||||
c, stdin, stdout, err := runCmd(
|
||||
"ssh",
|
||||
"-v",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-p",
|
||||
piperport,
|
||||
"-l",
|
||||
"user",
|
||||
"127.0.0.1",
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("failed to ssh to piper-fixed, %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(c)
|
||||
|
||||
enterPassword(stdin, stdout, "wrongpass1")
|
||||
enterPassword(stdin, stdout, "wrongpass2")
|
||||
enterPassword(stdin, stdout, "wrongpass3")
|
||||
}
|
||||
|
||||
{
|
||||
randtext := uuid.New().String()
|
||||
targetfie := uuid.New().String()
|
||||
|
||||
c, stdin, stdout, err := runCmd(
|
||||
"ssh",
|
||||
"-v",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-p",
|
||||
piperport,
|
||||
"-l",
|
||||
"user",
|
||||
"127.0.0.1",
|
||||
fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to ssh to piper-workingdir, %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(c)
|
||||
|
||||
enterPassword(stdin, stdout, "pass")
|
||||
|
||||
time.Sleep(time.Second) // wait for file flush
|
||||
|
||||
checkSharedFileContent(t, targetfie, randtext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailtobanIgnoreCIDR(t *testing.T) {
|
||||
piperaddr, piperport := nextAvailablePiperAddress()
|
||||
|
||||
piper, _, _, err := runCmd("/sshpiperd/sshpiperd",
|
||||
"-p",
|
||||
piperport,
|
||||
"/sshpiperd/plugins/fixed",
|
||||
"--target",
|
||||
"host-password:2222",
|
||||
"--",
|
||||
"/sshpiperd/plugins/failtoban",
|
||||
"--max-failures",
|
||||
"3",
|
||||
"--ignore-ip",
|
||||
"127.0.0.1/8",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to run sshpiperd: %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(piper)
|
||||
|
||||
waitForEndpointReady(piperaddr)
|
||||
|
||||
{
|
||||
randtext := uuid.New().String()
|
||||
targetfie := uuid.New().String()
|
||||
|
||||
c, stdin, stdout, err := runCmd(
|
||||
"ssh",
|
||||
"-v",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-p",
|
||||
piperport,
|
||||
"-l",
|
||||
"user",
|
||||
"127.0.0.1",
|
||||
fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to ssh to piper-workingdir, %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(c)
|
||||
|
||||
enterPassword(stdin, stdout, "pass")
|
||||
|
||||
time.Sleep(time.Second) // wait for file flush
|
||||
|
||||
checkSharedFileContent(t, targetfie, randtext)
|
||||
}
|
||||
|
||||
// run 3 times with wrong password
|
||||
{
|
||||
c, stdin, stdout, err := runCmd(
|
||||
"ssh",
|
||||
"-v",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-p",
|
||||
piperport,
|
||||
"-l",
|
||||
"user",
|
||||
"127.0.0.1",
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("failed to ssh to piper-fixed, %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(c)
|
||||
|
||||
enterPassword(stdin, stdout, "wrongpass1")
|
||||
enterPassword(stdin, stdout, "wrongpass2")
|
||||
enterPassword(stdin, stdout, "wrongpass3")
|
||||
}
|
||||
|
||||
{
|
||||
randtext := uuid.New().String()
|
||||
targetfie := uuid.New().String()
|
||||
|
||||
c, stdin, stdout, err := runCmd(
|
||||
"ssh",
|
||||
"-v",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-p",
|
||||
piperport,
|
||||
"-l",
|
||||
"user",
|
||||
"127.0.0.1",
|
||||
fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to ssh to piper-workingdir, %v", err)
|
||||
}
|
||||
|
||||
defer killCmd(c)
|
||||
|
||||
enterPassword(stdin, stdout, "pass")
|
||||
|
||||
time.Sleep(time.Second) // wait for file flush
|
||||
|
||||
checkSharedFileContent(t, targetfie, randtext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -44,6 +44,7 @@ require (
|
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -174,6 +174,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC
|
|||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"go4.org/netipx"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
|
|
@ -40,14 +43,21 @@ func main() {
|
|||
EnvVars: []string{"SSHPIPERD_FAILTOBAN_LOG_ONLY"},
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "ignore-ip",
|
||||
Usage: "ignore ip, will not ban host matches from these ip addresses",
|
||||
EnvVars: []string{"SSHPIPERD_FAILTOBAN_IGNORE_IP"},
|
||||
Value: cli.NewStringSlice(),
|
||||
},
|
||||
},
|
||||
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
||||
|
||||
maxFailures := c.Int("max-failures")
|
||||
banDuration := c.Duration("ban-duration")
|
||||
logOnly := c.Bool("log-only")
|
||||
|
||||
ignoreIP := c.StringSlice("ignore-ip")
|
||||
cache := gocache.New(banDuration, banDuration/2*3)
|
||||
whitelist := buildIPSet(ignoreIP)
|
||||
|
||||
// register signal handler
|
||||
go func() {
|
||||
|
|
@ -74,6 +84,12 @@ func main() {
|
|||
}
|
||||
|
||||
ip, _, _ := net.SplitHostPort(conn.RemoteAddr())
|
||||
ip0, _ := netip.ParseAddr(ip)
|
||||
|
||||
if whitelist.Contains(ip0) {
|
||||
log.Debugf("failtoban: %v in whitelist, ignored.", ip0)
|
||||
return nil
|
||||
}
|
||||
|
||||
failed, found := cache.Get(ip)
|
||||
if !found {
|
||||
|
|
@ -89,11 +105,25 @@ func main() {
|
|||
},
|
||||
UpstreamAuthFailureCallback: func(conn libplugin.ConnMetadata, method string, err error, allowmethods []string) {
|
||||
ip, _, _ := net.SplitHostPort(conn.RemoteAddr())
|
||||
ip0, _ := netip.ParseAddr(ip)
|
||||
|
||||
if whitelist.Contains(ip0) {
|
||||
log.Debugf("failtoban: %v in whitelist, ignored.", ip0)
|
||||
return
|
||||
}
|
||||
|
||||
failed, _ := cache.IncrementInt(ip, 1)
|
||||
log.Warnf("failtoban: %v auth failed. current status: fail %v times, max allowed %v", ip, failed, maxFailures)
|
||||
},
|
||||
PipeCreateErrorCallback: func(remoteAddr string, err error) {
|
||||
ip, _, _ := net.SplitHostPort(remoteAddr)
|
||||
ip0, _ := netip.ParseAddr(ip)
|
||||
|
||||
if whitelist.Contains(ip0) {
|
||||
log.Debugf("failtoban: %v in whitelist, ignored.", ip0)
|
||||
return
|
||||
}
|
||||
|
||||
failed, _ := cache.IncrementInt(ip, 1)
|
||||
log.Warnf("failtoban: %v pipe create failed, reason %v. current status: fail %v times, max allowed %v", ip, err, failed, maxFailures)
|
||||
},
|
||||
|
|
@ -101,3 +131,31 @@ func main() {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
func buildIPSet(cidrs []string) *netipx.IPSet {
|
||||
|
||||
var ipsetBuilder netipx.IPSetBuilder
|
||||
|
||||
for _, cidr := range cidrs {
|
||||
if strings.Contains(cidr, "/") {
|
||||
prefix, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
log.Debugf("failtoban: error while parsing ignore IP: \n%v", err)
|
||||
continue
|
||||
}
|
||||
ipsetBuilder.AddPrefix(prefix)
|
||||
} else {
|
||||
ip, err := netip.ParseAddr(cidr)
|
||||
if err != nil {
|
||||
log.Debugf("failtoban: error while parsing ignore IP: \n%v", err)
|
||||
continue
|
||||
}
|
||||
ipsetBuilder.Add(ip)
|
||||
}
|
||||
}
|
||||
ipset, err := ipsetBuilder.IPSet()
|
||||
if err != nil {
|
||||
log.Debugf("failtoban: error while getting IPSet: \n%v", err)
|
||||
}
|
||||
return ipset
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue