Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ _only_ be used _concurrently_ with Fail2ban functionality (if you are looking
for a way to allowlist or denylist IPs without using any of the Fail2ban
logic, you might want to use a different plugin.)

### Shared Jail

By default, each middleware instance has its own jail. To share a jail across multiple routers using the same middleware name, set `sharedJail` to `true`:

```yml
testData:
sharedJail: true
```

When enabled, all routers using the same named middleware will share the same jail, allowing bans to propagate across subdomains.

### Allowlist
You can allowlist some IP using this:
```yml
Expand Down
37 changes: 33 additions & 4 deletions fail2ban.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@

import (
"context"
"crypto/md5"

Check failure on line 6 in fail2ban.go

View workflow job for this annotation

GitHub Actions / Main Process

G501: Blocklisted import crypto/md5: weak cryptographic primitive (gosec)
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"sync"

"github.com/tomMoulard/fail2ban/pkg/chain"
"github.com/tomMoulard/fail2ban/pkg/fail2ban"
Expand All @@ -25,6 +28,11 @@
log.SetOutput(os.Stdout)
}

var (
globalJails = make(map[string]*fail2ban.Fail2Ban)
globalMu sync.Mutex
)

// List struct.
type List struct {
IP []string
Expand All @@ -33,9 +41,10 @@

// Config struct.
type Config struct {
Denylist List `yaml:"denylist"`

Check failure on line 44 in fail2ban.go

View workflow job for this annotation

GitHub Actions / Main Process

File is not properly formatted (gci)
Allowlist List `yaml:"allowlist"`
Rules rules.Rules `yaml:"port"`
SharedJail bool `yaml:"sharedJail"`

// deprecated
Blacklist List `yaml:"blacklist"`
Expand Down Expand Up @@ -77,7 +86,7 @@

// New instantiates and returns the required components used to handle a HTTP
// request.
func New(_ context.Context, next http.Handler, config *Config, _ string) (http.Handler, error) {
func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
if !config.Rules.Enabled {
log.Println("Plugin: FailToBan is disabled")

Expand Down Expand Up @@ -136,9 +145,29 @@
return nil, fmt.Errorf("error when Transforming rules: %w", err)
}

log.Println("Plugin: FailToBan is up and running")

f2b := fail2ban.New(rules, allowNetIPs)
// Get or create jail
var f2b *fail2ban.Fail2Ban
if config.SharedJail {
// Use middleware name and config hash as key for shared jail
keyBytes, _ := json.Marshal(config)

Check failure on line 152 in fail2ban.go

View workflow job for this annotation

GitHub Actions / Main Process

Error return value of `encoding/json.Marshal` is not checked (errchkjson)
jailKey := fmt.Sprintf("%s-%x", name, md5.Sum(keyBytes))

Check failure on line 153 in fail2ban.go

View workflow job for this annotation

GitHub Actions / Main Process

G401: Use of weak cryptographic primitive (gosec)
// Use shared jail based on middleware name
globalMu.Lock()
var exists bool
f2b, exists = globalJails[jailKey]
if !exists {

Check failure on line 158 in fail2ban.go

View workflow job for this annotation

GitHub Actions / Main Process

only one cuddle assignment allowed before if statement (wsl)
f2b = fail2ban.New(rules, allowNetIPs)
globalJails[jailKey] = f2b
log.Printf("Plugin: FailToBan created new shared jail for middleware %s", jailKey)

Check failure on line 161 in fail2ban.go

View workflow job for this annotation

GitHub Actions / Main Process

only cuddled expressions if assigning variable or using from line above (wsl)
} else {
log.Printf("Plugin: FailToBan using existing shared jail for middleware %s", jailKey)
}
globalMu.Unlock()
} else {
// Create individual jail
f2b = fail2ban.New(rules, allowNetIPs)
log.Printf("Plugin: FailToBan created individual jail for middleware %s", name)
}

c := chain.New(
next,
Expand Down
86 changes: 86 additions & 0 deletions fail2ban_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
func TestFail2Ban(t *testing.T) {
t.Parallel()

remoteAddr := "10.0.0.0"

Check failure on line 123 in fail2ban_test.go

View workflow job for this annotation

GitHub Actions / Main Process

string `10.0.0.0` has 3 occurrences, make it a constant (goconst)
tests := []struct {
name string
url string
Expand Down Expand Up @@ -503,3 +503,89 @@
})
}
}

func TestFail2Ban_SuccessiveRequests_SharedJail(t *testing.T) {
t.Parallel()

remoteAddr := "10.0.0.0"
tests := []struct {
name string

Check failure on line 512 in fail2ban_test.go

View workflow job for this annotation

GitHub Actions / Main Process

File is not properly formatted (gci)
cfg *Config
handlerStatus []int // HTTP code the internal HTTP handler should return
expectStatus []int // HTTP code the downstream client should request after passing through fail2ban
expectStatusSecond int
}{
{
name: "shared jail enabled propagates ban",
cfg: &Config{
Rules: rules.Rules{
Enabled: true,
Bantime: "300s",
Findtime: "300s",
Maxretry: 3,
StatusCode: "404",
},
SharedJail: true,
},
// the remaining OKs will not reach the client as it is banned
handlerStatus: []int{http.StatusNotFound, http.StatusOK, http.StatusNotFound, http.StatusNotFound, http.StatusOK},

Check failure on line 531 in fail2ban_test.go

View workflow job for this annotation

GitHub Actions / Main Process

File is not properly formatted (gci)
expectStatus: []int{http.StatusNotFound, http.StatusOK, http.StatusNotFound, http.StatusTooManyRequests, http.StatusTooManyRequests},
expectStatusSecond: http.StatusTooManyRequests,
},
{
name: "shared jail disabled does not propagate ban",
cfg: &Config{
Rules: rules.Rules{
Enabled: true,
Bantime: "300s",
Findtime: "300s",
Maxretry: 3,
StatusCode: "404",
},
SharedJail: false,
},
handlerStatus: []int{http.StatusNotFound, http.StatusOK, http.StatusNotFound, http.StatusNotFound, http.StatusOK},

Check failure on line 547 in fail2ban_test.go

View workflow job for this annotation

GitHub Actions / Main Process

File is not properly formatted (gci)
expectStatus: []int{http.StatusNotFound, http.StatusOK, http.StatusNotFound, http.StatusTooManyRequests, http.StatusTooManyRequests},
expectStatusSecond: http.StatusOK,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
jailKey := t.Name()
globalMu.Lock()
delete(globalJails, jailKey)
globalMu.Unlock()

next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testno, err := strconv.Atoi(r.Header.Get("Testno"))
assert.NoError(t, err)

w.WriteHeader(testno)
})

handler, err := New(t.Context(), next, test.cfg, jailKey)
require.NoError(t, err)
handler2, err := New(t.Context(), next, test.cfg, jailKey)
require.NoError(t, err)

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = remoteAddr + ":1234"

for i := range test.handlerStatus {
rw := httptest.NewRecorder()

req.Header.Set("Testno", strconv.Itoa(test.handlerStatus[i])) // pass the expected value to the mock handler (fail2ban response code may differ)
handler.ServeHTTP(rw, req)

assert.Equal(t, test.expectStatus[i], rw.Code, "request [%d] code", i)
}

rw := httptest.NewRecorder()
req.Header.Set("Testno", strconv.Itoa(http.StatusOK))
handler2.ServeHTTP(rw, req)
assert.Equal(t, test.expectStatusSecond, rw.Code, "request to handler2 code")
})
}
}