non return plugin callback now will be triggered together (#162)
* add failtoban plugin * fmt * bump ver * add failtoban e2e * add readme * update readme
This commit is contained in:
parent
bffa03dde0
commit
2067fcb003
6 changed files with 189 additions and 10 deletions
|
|
@ -94,6 +94,7 @@ Plugin list
|
||||||
* [simplemath](plugin/simplemath/) 🔒: ask for very simple math question before login, demo purpose
|
* [simplemath](plugin/simplemath/) 🔒: ask for very simple math question before login, demo purpose
|
||||||
* [githubapp](https://github.com/tg123/sshpiper-gh) 🔀: login ssh with your github account
|
* [githubapp](https://github.com/tg123/sshpiper-gh) 🔀: login ssh with your github account
|
||||||
* [restful](https://github.com/11notes/docker-sshpiper) by [@11notes](https://github.com/11notes) 🔀🔒: The rest plugin for sshpiperd is a simple plugin that allows you to use a restful backend for authentication and challenge.
|
* [restful](https://github.com/11notes/docker-sshpiper) by [@11notes](https://github.com/11notes) 🔀🔒: The rest plugin for sshpiperd is a simple plugin that allows you to use a restful backend for authentication and challenge.
|
||||||
|
* [failtoban](plugin/failtoban/) 🔒: ban ip after failed login attempts
|
||||||
|
|
||||||
## Screening recording
|
## Screening recording
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,10 @@ func (cp *ChainPlugins) InstallPiperConfig(config *GrpcPluginConfig) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.UpstreamAuthFailureCallback = func(conn ssh.ConnMetadata, method string, err error, challengeCtx ssh.ChallengeContext) {
|
config.UpstreamAuthFailureCallback = func(conn ssh.ConnMetadata, method string, err error, challengeCtx ssh.ChallengeContext) {
|
||||||
cur := cp.pluginsCallback[challengeCtx.(*chainConnMeta).current]
|
for _, p := range cp.pluginsCallback {
|
||||||
if cur.UpstreamAuthFailureCallback != nil {
|
if p.UpstreamAuthFailureCallback != nil {
|
||||||
cur.UpstreamAuthFailureCallback(conn, method, err, challengeCtx)
|
p.UpstreamAuthFailureCallback(conn, method, err, challengeCtx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,16 +142,18 @@ func (cp *ChainPlugins) InstallPiperConfig(config *GrpcPluginConfig) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.PipeStartCallback = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) {
|
config.PipeStartCallback = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext) {
|
||||||
cur := cp.pluginsCallback[challengeCtx.(*chainConnMeta).current]
|
for _, p := range cp.pluginsCallback {
|
||||||
if cur.PipeStartCallback != nil {
|
if p.PipeStartCallback != nil {
|
||||||
cur.PipeStartCallback(conn, challengeCtx)
|
p.PipeStartCallback(conn, challengeCtx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.PipeErrorCallback = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext, err error) {
|
config.PipeErrorCallback = func(conn ssh.ConnMetadata, challengeCtx ssh.ChallengeContext, err error) {
|
||||||
cur := cp.pluginsCallback[challengeCtx.(*chainConnMeta).current]
|
for _, p := range cp.pluginsCallback {
|
||||||
if cur.PipeErrorCallback != nil {
|
if p.PipeErrorCallback != nil {
|
||||||
cur.PipeErrorCallback(conn, challengeCtx, err)
|
p.PipeErrorCallback(conn, challengeCtx, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
86
e2e/failtoban_test.go
Normal file
86
e2e/failtoban_test.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package e2e_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFailtoban(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",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to run sshpiperd: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer killCmd(piper)
|
||||||
|
|
||||||
|
waitForEndpointReady(piperaddr)
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
c, _, stdout, err := runCmd(
|
||||||
|
"ssh",
|
||||||
|
"-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)
|
||||||
|
_ = c.Wait()
|
||||||
|
|
||||||
|
s, _ := io.ReadAll(stdout)
|
||||||
|
|
||||||
|
if !strings.Contains(string(s), "Connection closed by 127.0.0.1") {
|
||||||
|
t.Errorf("expected connection closed by")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
plugin/failtoban/README.md
Normal file
17
plugin/failtoban/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# fail to ban for sshpiperd
|
||||||
|
|
||||||
|
put ip to jail for a while after failed to login for several times.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
put this plugin after other plugins, like:
|
||||||
|
|
||||||
|
```
|
||||||
|
sshpiperd <main plguin> -- failtoban
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
* max-failures: max failures before ban, default 5
|
||||||
|
* ban-duration: ban duration, default 1h
|
||||||
72
plugin/failtoban/main.go
Normal file
72
plugin/failtoban/main.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
//go:build full || e2e
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gocache "github.com/patrickmn/go-cache"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/tg123/sshpiper/libplugin"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
||||||
|
Name: "failtoban",
|
||||||
|
Usage: "sshpiperd fixed plugin, only password auth is supported",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max-failures",
|
||||||
|
Usage: "max failures",
|
||||||
|
EnvVars: []string{"SSHPIPERD_FAILTOBAN_MAX_FAILURES"},
|
||||||
|
Value: 5,
|
||||||
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "ban-duration",
|
||||||
|
Usage: "ban duration",
|
||||||
|
EnvVars: []string{"SSHPIPERD_FAILTOBAN_BAN_DURATION"},
|
||||||
|
Value: 60 * time.Minute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
||||||
|
|
||||||
|
maxFailures := c.Int("max-failures")
|
||||||
|
banDuration := c.Duration("ban-duration")
|
||||||
|
|
||||||
|
cache := gocache.New(banDuration, banDuration/2*3)
|
||||||
|
|
||||||
|
return &libplugin.SshPiperPluginConfig{
|
||||||
|
NoClientAuthCallback: func(conn libplugin.ConnMetadata) (*libplugin.Upstream, error) {
|
||||||
|
// in case someone put the failtoban plugin before other plugins
|
||||||
|
return &libplugin.Upstream{
|
||||||
|
Auth: libplugin.CreateNextPluginAuth(map[string]string{}),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
NewConnectionCallback: func(conn libplugin.ConnMetadata) error {
|
||||||
|
ip, _, _ := net.SplitHostPort(conn.RemoteAddr())
|
||||||
|
|
||||||
|
failed, found := cache.Get(ip)
|
||||||
|
if !found {
|
||||||
|
// init
|
||||||
|
return cache.Add(ip, 0, banDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed.(int) >= maxFailures {
|
||||||
|
return fmt.Errorf("failtoban: ip %v too auth many failures", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
UpstreamAuthFailureCallback: func(conn libplugin.ConnMetadata, method string, err error, allowmethods []string) {
|
||||||
|
ip, _, _ := net.SplitHostPort(conn.RemoteAddr())
|
||||||
|
failed, _ := cache.IncrementInt(ip, 1)
|
||||||
|
log.Debugf("failtoban: %v auth failed %v times, max allowed %v", ip, failed, maxFailures)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
||||||
"version": "1.0",
|
"version": "1.1",
|
||||||
"publicReleaseRefSpec": [
|
"publicReleaseRefSpec": [
|
||||||
"^refs/heads/master$",
|
"^refs/heads/master$",
|
||||||
"^refs/tags/v\\d+\\.\\d+\\.\\d+"
|
"^refs/tags/v\\d+\\.\\d+\\.\\d+"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue