mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-07-21 11:38:59 +02:00
✨ Standalone mode come back (#74)
* ✨ Standalone mode come back * 🍱 fix lint * 🐛 fix tests cache * 🐛 fix tests * 🐛 fix tests * 🚨 fix lint * 🚨 fix lint * 🍱 add logging * :rotating-light: fix lint * 🍱 fix comments * 🐛 fix tests * 🚨 Fix lint Co-authored-by: Mathieu HANOTAUX <mathieu@hanotaux.fr>
This commit is contained in:
co-authored by
Mathieu HANOTAUX
parent
87a839f22d
commit
1b310b2c15
@@ -24,6 +24,7 @@ There are 3 operating modes (CrowdsecMode) for this plugin:
|
|||||||
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
|
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
|
||||||
| live | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries. |
|
| live | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries. |
|
||||||
| stream | Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI. |
|
| stream | Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI. |
|
||||||
|
| alone | Standalone mode, similar to the streaming mode but the blacklisted IPs are fetched on the CAPI. Every 2 hours, the cache is updated with news from the Crowdsec CAPI. It does not include any localy banned IP, but can work without a crowdsec service. |
|
||||||
|
|
||||||
The streaming mode is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between traefik and crowdsec. Every request that happens hits the cache for quick decisions.
|
The streaming mode is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between traefik and crowdsec. Every request that happens hits the cache for quick decisions.
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ make run
|
|||||||
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
||||||
- CrowdsecMode
|
- CrowdsecMode
|
||||||
- string
|
- string
|
||||||
- default: `live`, expected values are: `none`, `live`, `stream`
|
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`
|
||||||
- CrowdsecLapiScheme
|
- CrowdsecLapiScheme
|
||||||
- string
|
- string
|
||||||
- default: `http`, expected values are: `http`, `https`
|
- default: `http`, expected values are: `http`, `https`
|
||||||
@@ -83,14 +84,6 @@ make run
|
|||||||
- string
|
- string
|
||||||
- default: ""
|
- default: ""
|
||||||
- PEM-encoded client private key of the Bouncer
|
- PEM-encoded client private key of the Bouncer
|
||||||
- UpdateIntervalSeconds
|
|
||||||
- int64
|
|
||||||
- default: 60
|
|
||||||
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
|
|
||||||
- DefaultDecisionSeconds
|
|
||||||
- int64
|
|
||||||
- default: 60
|
|
||||||
- Used only in `live` mode, decision duration of accepted IPs
|
|
||||||
- ClientTrustedIPs
|
- ClientTrustedIPs
|
||||||
- string
|
- string
|
||||||
- default: []
|
- default: []
|
||||||
@@ -111,6 +104,23 @@ make run
|
|||||||
- string
|
- string
|
||||||
- default: "redis:6379"
|
- default: "redis:6379"
|
||||||
- hostname and port for the redis service
|
- hostname and port for the redis service
|
||||||
|
- UpdateIntervalSeconds
|
||||||
|
- int64
|
||||||
|
- default: 60
|
||||||
|
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
|
||||||
|
- DefaultDecisionSeconds
|
||||||
|
- int64
|
||||||
|
- default: 60
|
||||||
|
- Used only in `live` mode, decision duration of accepted IPs
|
||||||
|
- CrowdsecCapiMachineId
|
||||||
|
- string
|
||||||
|
- Used only in `alone` mode, login for Crowdsec CAPI
|
||||||
|
- CrowdsecCapiPassword
|
||||||
|
- string
|
||||||
|
- Used only in `alone` mode, password for Crowdsec CAPI
|
||||||
|
- CrowdsecCapiScenarios
|
||||||
|
- []string
|
||||||
|
- Used only in `alone` mode, scenarios for Crowdsec CAPI
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@@ -160,6 +170,12 @@ http:
|
|||||||
crowdsecLapiHost: crowdsec:8080
|
crowdsecLapiHost: crowdsec:8080
|
||||||
crowdsecLapiScheme: http
|
crowdsecLapiScheme: http
|
||||||
crowdsecLapiTLSInsecureVerify: false
|
crowdsecLapiTLSInsecureVerify: false
|
||||||
|
crowdsecCapiMachineId: login
|
||||||
|
crowdsecCapiPassword: password
|
||||||
|
crowdsecCapiScenarios:
|
||||||
|
- crowdsecurity/http-path-traversal-probing
|
||||||
|
- crowdsecurity/http-xss-probing
|
||||||
|
- crowdsecurity/http-generic-bf
|
||||||
forwardedHeadersTrustedIPs:
|
forwardedHeadersTrustedIPs:
|
||||||
- 10.0.10.23/32
|
- 10.0.10.23/32
|
||||||
- 10.0.20.0/24
|
- 10.0.20.0/24
|
||||||
@@ -190,12 +206,11 @@ http:
|
|||||||
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
|
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
|
||||||
-----END RSA PRIVATE KEY-----
|
-----END RSA PRIVATE KEY-----
|
||||||
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
|
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Fill variable with value of file
|
#### Fill variable with value of file
|
||||||
|
|
||||||
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority` and `CrowdsecLapiKey` can be provided with the content as raw or through a file path that Traefik can read.
|
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword` and `CrowdsecLapiKey` can be provided with the content as raw or through a file path that Traefik can read.
|
||||||
The file variable will be used as preference if both content and file are provided for the same variable.
|
The file variable will be used as preference if both content and file are provided for the same variable.
|
||||||
|
|
||||||
Format is:
|
Format is:
|
||||||
|
|||||||
+125
-38
@@ -5,6 +5,7 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -24,14 +25,17 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
crowdsecLapiHeader = "X-Api-Key"
|
crowdsecLapiHeader = "X-Api-Key"
|
||||||
|
crowdsecCapiHeader = "Authorization"
|
||||||
crowdsecLapiRoute = "v1/decisions"
|
crowdsecLapiRoute = "v1/decisions"
|
||||||
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
||||||
|
crowdsecCapiLogin = "v2/watchers/login"
|
||||||
|
crowdsecCapiStreamRoute = "v2/decisions/stream"
|
||||||
cacheTimeoutKey = "updated"
|
cacheTimeoutKey = "updated"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
var (
|
var (
|
||||||
isCrowdsecStreamHealthy = false
|
isCrowdsecStreamHealthy = true
|
||||||
ticker chan bool
|
ticker chan bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,12 +55,18 @@ type Bouncer struct {
|
|||||||
crowdsecHost string
|
crowdsecHost string
|
||||||
crowdsecKey string
|
crowdsecKey string
|
||||||
crowdsecMode string
|
crowdsecMode string
|
||||||
|
crowdsecMachineID string
|
||||||
|
crowdsecPassword string
|
||||||
|
crowdsecScenarios []string
|
||||||
updateInterval int64
|
updateInterval int64
|
||||||
defaultDecisionTimeout int64
|
defaultDecisionTimeout int64
|
||||||
customHeader string
|
customHeader string
|
||||||
|
crowdsecStreamRoute string
|
||||||
|
crowdsecHeader string
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
client *http.Client
|
httpClient *http.Client
|
||||||
|
cacheClient *cache.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// New creates the crowdsec bouncer plugin.
|
||||||
@@ -64,24 +74,39 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
logger.Init(config.LogLevel)
|
logger.Init(config.LogLevel)
|
||||||
err := configuration.ValidateParams(config)
|
err := configuration.ValidateParams(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
|
logger.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||||
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
|
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
|
||||||
|
|
||||||
tlsConfig, err := configuration.GetTLSConfigCrowdsec(config)
|
var tlsConfig *tls.Config
|
||||||
if err != nil {
|
crowdsecStreamRoute := ""
|
||||||
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
crowdsecHeader := ""
|
||||||
return nil, err
|
if config.CrowdsecMode == configuration.AloneMode {
|
||||||
|
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineId")
|
||||||
|
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
|
||||||
|
config.CrowdsecLapiHost = "api.crowdsec.net"
|
||||||
|
config.CrowdsecLapiScheme = "https"
|
||||||
|
config.UpdateIntervalSeconds = 7200
|
||||||
|
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
||||||
|
crowdsecHeader = crowdsecCapiHeader
|
||||||
|
} else {
|
||||||
|
crowdsecStreamRoute = crowdsecLapiStreamRoute
|
||||||
|
crowdsecHeader = crowdsecLapiHeader
|
||||||
|
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||||
|
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
|
||||||
|
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.CrowdsecLapiKey = apiKey
|
||||||
}
|
}
|
||||||
apiKey, err := configuration.GetVariable(config, "CrowdsecLapiKey")
|
|
||||||
if err != nil && len(tlsConfig.Certificates) == 0 {
|
|
||||||
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", err.Error()))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
apiKey = strings.TrimSuffix(apiKey, "\n")
|
|
||||||
|
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
next: next,
|
next: next,
|
||||||
@@ -92,17 +117,22 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
crowdsecMode: config.CrowdsecMode,
|
crowdsecMode: config.CrowdsecMode,
|
||||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||||
crowdsecHost: config.CrowdsecLapiHost,
|
crowdsecHost: config.CrowdsecLapiHost,
|
||||||
crowdsecKey: apiKey,
|
crowdsecKey: config.CrowdsecLapiKey,
|
||||||
|
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
||||||
|
crowdsecPassword: config.CrowdsecCapiPassword,
|
||||||
|
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
||||||
updateInterval: config.UpdateIntervalSeconds,
|
updateInterval: config.UpdateIntervalSeconds,
|
||||||
customHeader: config.ForwardedHeadersCustomName,
|
customHeader: config.ForwardedHeadersCustomName,
|
||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
|
crowdsecHeader: crowdsecHeader,
|
||||||
serverPoolStrategy: &ip.PoolStrategy{
|
serverPoolStrategy: &ip.PoolStrategy{
|
||||||
Checker: serverChecker,
|
Checker: serverChecker,
|
||||||
},
|
},
|
||||||
clientPoolStrategy: &ip.PoolStrategy{
|
clientPoolStrategy: &ip.PoolStrategy{
|
||||||
Checker: clientChecker,
|
Checker: clientChecker,
|
||||||
},
|
},
|
||||||
client: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
@@ -110,16 +140,24 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
},
|
},
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
},
|
},
|
||||||
|
cacheClient: &cache.Client{},
|
||||||
}
|
}
|
||||||
if config.RedisCacheEnabled {
|
bouncer.cacheClient.New(config.RedisCacheEnabled, config.RedisCacheHost)
|
||||||
cache.InitRedisClient(config.RedisCacheHost)
|
|
||||||
}
|
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
|
||||||
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
|
if config.CrowdsecMode == configuration.AloneMode {
|
||||||
|
err = getToken(bouncer)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("New:getToken %s", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
ticker = startTicker(config, func() {
|
ticker = startTicker(config, func() {
|
||||||
handleStreamCache(bouncer)
|
handleStreamCache(bouncer)
|
||||||
})
|
})
|
||||||
go handleStreamCache(bouncer)
|
go handleStreamCache(bouncer)
|
||||||
}
|
}
|
||||||
|
logger.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
|
||||||
|
|
||||||
return bouncer, nil
|
return bouncer, nil
|
||||||
}
|
}
|
||||||
@@ -155,9 +193,9 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
// TODO This should be simplified
|
// TODO This should be simplified
|
||||||
if bouncer.crowdsecMode != configuration.NoneMode {
|
if bouncer.crowdsecMode != configuration.NoneMode {
|
||||||
isBanned, erro := cache.GetDecision(remoteIP)
|
isBanned, erro := bouncer.cacheClient.GetDecision(remoteIP)
|
||||||
if erro != nil {
|
if erro != nil {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, erro.Error()))
|
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:true %s", remoteIP, erro.Error()))
|
||||||
if erro.Error() == simpleredis.RedisUnreachable {
|
if erro.Error() == simpleredis.RedisUnreachable {
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
@@ -174,19 +212,20 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Right here if we cannot join the stream we forbid the request to go on.
|
// Right here if we cannot join the stream we forbid the request to go on.
|
||||||
if bouncer.crowdsecMode == configuration.StreamMode {
|
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
if isCrowdsecStreamHealthy {
|
if isCrowdsecStreamHealthy {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
} else {
|
} else {
|
||||||
logger.Error(fmt.Sprintf("ServeHTTP:isCrowdsecStreamHealthy ip:%s", remoteIP))
|
logger.Error(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = handleNoStreamCache(bouncer, remoteIP)
|
err = handleNoStreamCache(bouncer, remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s %s", remoteIP, err.Error()))
|
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
} else {
|
} else {
|
||||||
|
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP))
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +252,13 @@ type Stream struct {
|
|||||||
New []Decision `json:"new"`
|
New []Decision `json:"new"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login Body returned from Crowdsec Login CAPI.
|
||||||
|
type Login struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Expire string `json:"expire"`
|
||||||
|
}
|
||||||
|
|
||||||
func startTicker(config *configuration.Config, work func()) chan bool {
|
func startTicker(config *configuration.Config, work func()) chan bool {
|
||||||
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
||||||
stop := make(chan bool, 1)
|
stop := make(chan bool, 1)
|
||||||
@@ -239,14 +285,14 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
|||||||
Path: crowdsecLapiRoute,
|
Path: crowdsecLapiRoute,
|
||||||
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, routeURL.String())
|
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(body, []byte("null")) {
|
if bytes.Equal(body, []byte("null")) {
|
||||||
if isLiveMode {
|
if isLiveMode {
|
||||||
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -258,7 +304,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
|||||||
}
|
}
|
||||||
if len(decisions) == 0 {
|
if len(decisions) == 0 {
|
||||||
if isLiveMode {
|
if isLiveMode {
|
||||||
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -267,29 +313,53 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
|||||||
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
||||||
}
|
}
|
||||||
if isLiveMode {
|
if isLiveMode {
|
||||||
cache.SetDecision(remoteIP, true, int64(duration.Seconds()))
|
bouncer.cacheClient.SetDecision(remoteIP, true, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
return fmt.Errorf("handleNoStreamCache:banned")
|
return fmt.Errorf("handleNoStreamCache:banned")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getToken(bouncer *Bouncer) error {
|
||||||
|
loginURL := url.URL{
|
||||||
|
Scheme: bouncer.crowdsecScheme,
|
||||||
|
Host: bouncer.crowdsecHost,
|
||||||
|
Path: crowdsecCapiLogin,
|
||||||
|
}
|
||||||
|
body, err := crowdsecQuery(bouncer, loginURL.String(), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var login Login
|
||||||
|
err = json.Unmarshal(body, &login)
|
||||||
|
if err != nil {
|
||||||
|
isCrowdsecStreamHealthy = false
|
||||||
|
return fmt.Errorf("getToken:parsingBody %w", err)
|
||||||
|
}
|
||||||
|
if login.Code == 200 && len(login.Token) > 0 {
|
||||||
|
bouncer.crowdsecKey = login.Token
|
||||||
|
logger.Debug(fmt.Sprintf("getToken statusCode:%d", login.Code))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("getToken statusCode:%d", login.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func handleStreamCache(bouncer *Bouncer) {
|
func handleStreamCache(bouncer *Bouncer) {
|
||||||
// TODO clean properly on exit.
|
// TODO clean properly on exit.
|
||||||
// Instead of blocking the goroutine interval for all the secondary node,
|
// Instead of blocking the goroutine interval for all the secondary node,
|
||||||
// if the master service is shut down, other goroutine can take the lead
|
// if the master service is shut down, other goroutine can take the lead
|
||||||
// because updated routine information is in the cache
|
// because updated routine information is in the cache
|
||||||
_, err := cache.GetDecision(cacheTimeoutKey)
|
_, err := bouncer.cacheClient.GetDecision(cacheTimeoutKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Debug("handleStreamCache:alreadyUpdated")
|
logger.Debug("handleStreamCache:alreadyUpdated")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cache.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
|
bouncer.cacheClient.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
|
||||||
streamRouteURL := url.URL{
|
streamRouteURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: crowdsecLapiStreamRoute,
|
Path: bouncer.crowdsecStreamRoute,
|
||||||
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
|
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, streamRouteURL.String())
|
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err.Error())
|
logger.Error(err.Error())
|
||||||
isCrowdsecStreamHealthy = false
|
isCrowdsecStreamHealthy = false
|
||||||
@@ -305,23 +375,40 @@ func handleStreamCache(bouncer *Bouncer) {
|
|||||||
for _, decision := range stream.New {
|
for _, decision := range stream.New {
|
||||||
duration, err := time.ParseDuration(decision.Duration)
|
duration, err := time.ParseDuration(decision.Duration)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cache.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
bouncer.cacheClient.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, decision := range stream.Deleted {
|
for _, decision := range stream.Deleted {
|
||||||
cache.DeleteDecision(decision.Value)
|
bouncer.cacheClient.DeleteDecision(decision.Value)
|
||||||
}
|
}
|
||||||
|
logger.Debug("handleStreamCache:updated")
|
||||||
isCrowdsecStreamHealthy = true
|
isCrowdsecStreamHealthy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
|
func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, error) {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
if isPost {
|
||||||
req.Header.Add(crowdsecLapiHeader, bouncer.crowdsecKey)
|
data := []byte(fmt.Sprintf(
|
||||||
res, err := bouncer.client.Do(req)
|
`{"machine_id": "%v","password": "%v","scenarios": ["%v"]}`,
|
||||||
|
bouncer.crowdsecMachineID,
|
||||||
|
bouncer.crowdsecPassword,
|
||||||
|
strings.Join(bouncer.crowdsecScenarios, `","`),
|
||||||
|
))
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, stringURL, bytes.NewBuffer(data))
|
||||||
|
} else {
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
||||||
|
}
|
||||||
|
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
|
||||||
|
res, err := bouncer.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
|
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
|
||||||
}
|
}
|
||||||
|
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
|
if errToken := getToken(bouncer); errToken != nil {
|
||||||
|
return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken)
|
||||||
|
}
|
||||||
|
return crowdsecQuery(bouncer, stringURL, false)
|
||||||
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
|
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||||
)
|
)
|
||||||
@@ -77,7 +78,8 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
|||||||
customHeader string
|
customHeader string
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
client *http.Client
|
httpClient *http.Client
|
||||||
|
cacheClient *cache.Client
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
rw http.ResponseWriter
|
rw http.ResponseWriter
|
||||||
@@ -106,7 +108,8 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
|||||||
customHeader: tt.fields.customHeader,
|
customHeader: tt.fields.customHeader,
|
||||||
clientPoolStrategy: tt.fields.clientPoolStrategy,
|
clientPoolStrategy: tt.fields.clientPoolStrategy,
|
||||||
serverPoolStrategy: tt.fields.serverPoolStrategy,
|
serverPoolStrategy: tt.fields.serverPoolStrategy,
|
||||||
client: tt.fields.client,
|
httpClient: tt.fields.httpClient,
|
||||||
|
cacheClient: tt.fields.cacheClient,
|
||||||
}
|
}
|
||||||
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
|
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
|
||||||
})
|
})
|
||||||
@@ -155,6 +158,7 @@ func Test_crowdsecQuery(t *testing.T) {
|
|||||||
type args struct {
|
type args struct {
|
||||||
bouncer *Bouncer
|
bouncer *Bouncer
|
||||||
stringURL string
|
stringURL string
|
||||||
|
isPost bool
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -166,7 +170,7 @@ func Test_crowdsecQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := crowdsecQuery(tt.args.bouncer, tt.args.stringURL)
|
got, err := crowdsecQuery(tt.args.bouncer, tt.args.stringURL, tt.args.isPost)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.9.4"
|
image: "traefik:v2.9.6"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.9.4"
|
image: "traefik:v2.9.6"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.3"
|
- "--experimental.plugins.bouncer.version=v1.1.8"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
- "logs:/var/log/traefik"
|
- "logs:/var/log/traefik"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#### Generate CAPI credentials (only for `alone` mode)
|
||||||
|
You need to create a crowdsec API credentials for the CAPI.
|
||||||
|
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"PASSWORD\", \"machine_id\": \"LOGIN\"}"
|
||||||
|
```
|
||||||
|
|
||||||
|
These CAPI credentials must be set in your docker-compose.yml or in your config files
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
whoami:
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiMachineId=LOGIN"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiPassword=PASSWORD"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/http-generic-bf,crowdsecurity/http-xss-probing,..."
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run all the containers:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
Vendored
+40
-38
@@ -18,14 +18,13 @@ const (
|
|||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
var (
|
var (
|
||||||
cache = ttl_map.New()
|
redis simpleredis.SimpleRedis
|
||||||
redis simpleredis.SimpleRedis
|
cache = ttl_map.New()
|
||||||
redisEnabled = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileSystem Cache
|
type localCache struct{}
|
||||||
|
|
||||||
func getDecisionLocalCache(clientIP string) (bool, error) {
|
func (localCache) getDecision(clientIP string) (bool, error) {
|
||||||
banned, isCached := cache.Get(clientIP)
|
banned, isCached := cache.Get(clientIP)
|
||||||
bannedString, isValid := banned.(string)
|
bannedString, isValid := banned.(string)
|
||||||
if isCached && isValid && len(bannedString) > 0 {
|
if isCached && isValid && len(bannedString) > 0 {
|
||||||
@@ -34,17 +33,17 @@ func getDecisionLocalCache(clientIP string) (bool, error) {
|
|||||||
return false, fmt.Errorf("cache:miss")
|
return false, fmt.Errorf("cache:miss")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDecisionLocalCache(clientIP string, value string, duration int64) {
|
func (localCache) setDecision(clientIP string, value string, duration int64) {
|
||||||
cache.Set(clientIP, value, duration)
|
cache.Set(clientIP, value, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteDecisionLocalCache(clientIP string) {
|
func (localCache) deleteDecision(clientIP string) {
|
||||||
cache.Del(clientIP)
|
cache.Del(clientIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redis Cache
|
type redisCache struct{}
|
||||||
|
|
||||||
func getDecisionRedisCache(clientIP string) (bool, error) {
|
func (redisCache) getDecision(clientIP string) (bool, error) {
|
||||||
banned, err := redis.Get(clientIP)
|
banned, err := redis.Get(clientIP)
|
||||||
bannedString := string(banned)
|
bannedString := string(banned)
|
||||||
if err == nil && len(bannedString) > 0 {
|
if err == nil && len(bannedString) > 0 {
|
||||||
@@ -53,58 +52,61 @@ func getDecisionRedisCache(clientIP string) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDecisionRedisCache(clientIP string, value string, duration int64) {
|
func (redisCache) setDecision(clientIP string, value string, duration int64) {
|
||||||
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
||||||
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteDecisionRedisCache(clientIP string) {
|
func (redisCache) deleteDecision(clientIP string) {
|
||||||
if err := redis.Del(clientIP); err != nil {
|
if err := redis.Del(clientIP); err != nil {
|
||||||
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDecision delete decision in cache.
|
type cacheInterface interface {
|
||||||
func DeleteDecision(clientIP string) {
|
setDecision(clientIP string, value string, duration int64)
|
||||||
logger.Debug("cache:DeleteDecision")
|
getDecision(clientIP string) (bool, error)
|
||||||
if redisEnabled {
|
deleteDecision(clientIP string)
|
||||||
deleteDecisionRedisCache(clientIP)
|
}
|
||||||
|
|
||||||
|
// Client Cache client.
|
||||||
|
type Client struct {
|
||||||
|
cache cacheInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Initialize cache client.
|
||||||
|
func (client *Client) New(isRedis bool, host string) {
|
||||||
|
if isRedis {
|
||||||
|
redis.Init(host)
|
||||||
|
client.cache = &redisCache{}
|
||||||
} else {
|
} else {
|
||||||
deleteDecisionLocalCache(clientIP)
|
client.cache = &localCache{}
|
||||||
}
|
}
|
||||||
|
logger.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDecision delete decision in cache.
|
||||||
|
func (client *Client) DeleteDecision(clientIP string) {
|
||||||
|
logger.Debug(fmt.Sprintf("cache:DeleteDecision ip:%v", clientIP))
|
||||||
|
client.cache.deleteDecision(clientIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDecision check in the cache if the IP has the banned / not banned value.
|
// GetDecision check in the cache if the IP has the banned / not banned value.
|
||||||
// Otherwise return with an error to add the IP in cache if we are on.
|
// Otherwise return with an error to add the IP in cache if we are on.
|
||||||
func GetDecision(clientIP string) (bool, error) {
|
func (client *Client) GetDecision(clientIP string) (bool, error) {
|
||||||
logger.Debug("cache:GetDecision")
|
logger.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
|
||||||
if redisEnabled {
|
return client.cache.getDecision(clientIP)
|
||||||
return getDecisionRedisCache(clientIP)
|
|
||||||
}
|
|
||||||
return getDecisionLocalCache(clientIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
||||||
func SetDecision(clientIP string, isBanned bool, duration int64) {
|
func (client *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
|
||||||
|
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v", clientIP, isBanned))
|
||||||
var value string
|
var value string
|
||||||
if isBanned {
|
if isBanned {
|
||||||
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v banned", clientIP))
|
|
||||||
value = cacheBannedValue
|
value = cacheBannedValue
|
||||||
} else {
|
} else {
|
||||||
value = cacheNoBannedValue
|
value = cacheNoBannedValue
|
||||||
}
|
}
|
||||||
logger.Debug("cache:SetDecision")
|
client.cache.setDecision(clientIP, value, duration)
|
||||||
if redisEnabled {
|
|
||||||
setDecisionRedisCache(clientIP, value, duration)
|
|
||||||
} else {
|
|
||||||
setDecisionLocalCache(clientIP, value, duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitRedisClient loads variables.
|
|
||||||
func InitRedisClient(host string) {
|
|
||||||
redisEnabled = true
|
|
||||||
redis.Init(host)
|
|
||||||
logger.Debug("cache:InitRedisClient redis:initialized")
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+38
-162
@@ -6,10 +6,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_getDecisionLocalCache(t *testing.T) {
|
func Test_GetDecision(t *testing.T) {
|
||||||
IPInCache := "10.0.0.10"
|
IPInCache := "10.0.0.10"
|
||||||
IPNotInCache := "10.0.0.20"
|
IPNotInCache := "10.0.0.20"
|
||||||
setDecisionLocalCache(IPInCache, "t", 10)
|
client := &Client{cache: &localCache{}}
|
||||||
|
client.SetDecision(IPInCache, true, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
}
|
}
|
||||||
@@ -22,205 +23,80 @@ func Test_getDecisionLocalCache(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: true, wantErr: false, valueErr: ""},
|
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: true, wantErr: false, valueErr: ""},
|
||||||
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false, wantErr: true, valueErr: "cache:miss"},
|
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||||
{name: "Fetch invalid value", args: args{clientIP: "zaeaea"}, want: false, wantErr: true, valueErr: "cache:miss"},
|
{name: "Fetch invalid value", args: args{clientIP: "test"}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||||
{name: "Fetch empty value", args: args{clientIP: ""}, want: false, wantErr: true, valueErr: "cache:miss"},
|
{name: "Fetch empty value", args: args{clientIP: ""}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := getDecisionLocalCache(tt.args.clientIP)
|
got, err := client.GetDecision(tt.args.clientIP)
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("getDecisionLocalCache() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("getDecisionLocalCache() = %v, want %v", got, tt.want)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
|
||||||
t.Errorf("getDecisionLocalCache() err = %v, want %v", err.Error(), tt.valueErr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_setDecisionLocalCache(t *testing.T) {
|
|
||||||
IPInCache := "10.0.0.10"
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
value string
|
|
||||||
duration int64
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
{name: "Set valid IP in local cache as t", args: args{clientIP: IPInCache, value: "t", duration: 0}},
|
|
||||||
{name: "Set valid IP in local cache as f", args: args{clientIP: IPInCache, value: "f", duration: 0}},
|
|
||||||
{name: "Set valid IP in local cache as empty str", args: args{clientIP: IPInCache, value: "", duration: 0}},
|
|
||||||
{name: "Set valid IP in local cache as f for -1 sec", args: args{clientIP: IPInCache, value: "f", duration: -1}},
|
|
||||||
{name: "Set valid IP in local cache as f for 10 sec", args: args{clientIP: IPInCache, value: "f", duration: 10}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
setDecisionLocalCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_deleteDecisionLocalCache(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
deleteDecisionLocalCache(tt.args.clientIP)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_getDecisionRedisCache(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := getDecisionRedisCache(tt.args.clientIP)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("getDecisionRedisCache() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("getDecisionRedisCache() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_setDecisionRedisCache(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
value string
|
|
||||||
duration int64
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
setDecisionRedisCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_deleteDecisionRedisCache(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
deleteDecisionRedisCache(tt.args.clientIP)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteDecision(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
DeleteDecision(tt.args.clientIP)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetDecision(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := GetDecision(tt.args.clientIP)
|
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("GetDecision() = %v, want %v", got, tt.want)
|
t.Errorf("GetDecision() = %v, want %v", got, tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||||
|
t.Errorf("GetDecision() err = %v, want %v", err.Error(), tt.valueErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetDecision(t *testing.T) {
|
func Test_SetDecision(t *testing.T) {
|
||||||
|
client := &Client{cache: &localCache{}}
|
||||||
|
IPInCache := "10.0.0.11"
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
isBanned bool
|
value bool
|
||||||
duration int64
|
duration int64
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
|
want bool
|
||||||
}{
|
}{
|
||||||
// TODO: Add test cases.
|
{name: "Set valid IP in local cache for 0 sec", args: args{clientIP: IPInCache, value: true, duration: 0}, want: false},
|
||||||
|
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: true, duration: 10}, want: true},
|
||||||
|
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: false, duration: 10}, want: false},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
SetDecision(tt.args.clientIP, tt.args.isBanned, tt.args.duration)
|
client.SetDecision(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||||
|
got, _ := client.GetDecision(tt.args.clientIP)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("SetDecision() = %v, want %v", got, tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitRedisClient(t *testing.T) {
|
func Test_DeleteDecision(t *testing.T) {
|
||||||
|
IPInCache := "10.0.0.12"
|
||||||
|
IPNotInCache := "10.0.0.22"
|
||||||
|
client := &Client{cache: &localCache{}}
|
||||||
|
client.SetDecision(IPInCache, true, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
host string
|
clientIP string
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
|
want bool
|
||||||
}{
|
}{
|
||||||
// TODO: Add test cases.
|
{name: "Delete Known valid IP", args: args{clientIP: IPInCache}, want: false},
|
||||||
|
{name: "Delete Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
InitRedisClient(tt.args.host)
|
client.DeleteDecision(tt.args.clientIP)
|
||||||
|
got, _ := client.GetDecision(tt.args.clientIP)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("DeleteDecision() = %v, want %v", got, tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
// Enums for crowdsec mode.
|
// Enums for crowdsec mode.
|
||||||
const (
|
const (
|
||||||
|
AloneMode = "alone"
|
||||||
StreamMode = "stream"
|
StreamMode = "stream"
|
||||||
LiveMode = "live"
|
LiveMode = "live"
|
||||||
NoneMode = "none"
|
NoneMode = "none"
|
||||||
@@ -42,6 +43,11 @@ type Config struct {
|
|||||||
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
|
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
|
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
|
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
|
||||||
|
CrowdsecCapiMachineID string `json:"crowdsecCapiMachineId,omitempty"`
|
||||||
|
CrowdsecCapiMachineIDFile string `json:"crowdsecCapiMachineIdFile,omitempty"`
|
||||||
|
CrowdsecCapiPassword string `json:"crowdsecCapiPassword,omitempty"`
|
||||||
|
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
|
||||||
|
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
|
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
|
||||||
@@ -100,11 +106,11 @@ func GetVariable(config *Config, key string) (string, error) {
|
|||||||
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
|
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
|
||||||
}
|
}
|
||||||
value = string(fileValue)
|
value = string(fileValue)
|
||||||
return value, nil
|
return strings.TrimSpace(value), nil
|
||||||
}
|
}
|
||||||
field = object.FieldByName(key)
|
field = object.FieldByName(key)
|
||||||
value = field.String()
|
value = field.String()
|
||||||
return value, nil
|
return strings.TrimSpace(value), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateParams validate all the param gave by user.
|
// ValidateParams validate all the param gave by user.
|
||||||
@@ -115,6 +121,23 @@ func ValidateParams(config *Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CrowdsecMode == AloneMode {
|
||||||
|
if _, err := GetVariable(config, "CrowdsecCapiMachineId"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := GetVariable(config, "CrowdsecCapiPassword"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// This only check that the format of the URL scheme:// is correct and do not make requests
|
// This only check that the format of the URL scheme:// is correct and do not make requests
|
||||||
testURL := url.URL{
|
testURL := url.URL{
|
||||||
Scheme: config.CrowdsecLapiScheme,
|
Scheme: config.CrowdsecLapiScheme,
|
||||||
@@ -124,13 +147,6 @@ func ValidateParams(config *Config) error {
|
|||||||
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
|
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -222,8 +238,8 @@ func validateParamsRequired(config *Config) error {
|
|||||||
return fmt.Errorf("%v: cannot be less than 1", key)
|
return fmt.Errorf("%v: cannot be less than 1", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !contains([]string{NoneMode, LiveMode, StreamMode}, config.CrowdsecMode) {
|
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode}, config.CrowdsecMode) {
|
||||||
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
|
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream' or 'alone'")
|
||||||
}
|
}
|
||||||
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
||||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||||
@@ -237,10 +253,10 @@ func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
|
|||||||
tlsConfig.RootCAs = x509.NewCertPool()
|
tlsConfig.RootCAs = x509.NewCertPool()
|
||||||
//nolint:gocritic
|
//nolint:gocritic
|
||||||
if config.CrowdsecLapiScheme != HTTPS {
|
if config.CrowdsecLapiScheme != HTTPS {
|
||||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme not https")
|
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
|
||||||
return tlsConfig, nil
|
return tlsConfig, nil
|
||||||
} else if config.CrowdsecLapiTLSInsecureVerify {
|
} else if config.CrowdsecLapiTLSInsecureVerify {
|
||||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify is true")
|
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
|
||||||
tlsConfig.InsecureSkipVerify = true
|
tlsConfig.InsecureSkipVerify = true
|
||||||
// If we return here and still want to use client auth this won't work
|
// If we return here and still want to use client auth this won't work
|
||||||
// return tlsConfig, nil
|
// return tlsConfig, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user