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:
maxlerebourg
2023-01-02 11:53:57 +01:00
committed by GitHub
co-authored by Mathieu HANOTAUX
parent 87a839f22d
commit 1b310b2c15
9 changed files with 291 additions and 268 deletions
+26 -11
View File
@@ -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 |
| 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. |
| 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.
@@ -55,7 +56,7 @@ make run
- default: `INFO`, expected values are: `INFO`, `DEBUG`
- CrowdsecMode
- string
- default: `live`, expected values are: `none`, `live`, `stream`
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`
- CrowdsecLapiScheme
- string
- default: `http`, expected values are: `http`, `https`
@@ -83,14 +84,6 @@ make run
- string
- default: ""
- 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
- string
- default: []
@@ -111,6 +104,23 @@ make run
- string
- default: "redis:6379"
- 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
@@ -160,6 +170,12 @@ http:
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
crowdsecLapiTLSInsecureVerify: false
crowdsecCapiMachineId: login
crowdsecCapiPassword: password
crowdsecCapiScenarios:
- crowdsecurity/http-path-traversal-probing
- crowdsecurity/http-xss-probing
- crowdsecurity/http-generic-bf
forwardedHeadersTrustedIPs:
- 10.0.10.23/32
- 10.0.20.0/24
@@ -190,12 +206,11 @@ http:
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
-----END RSA PRIVATE KEY-----
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
```
#### 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.
Format is:
+118 -31
View File
@@ -5,6 +5,7 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -24,14 +25,17 @@ import (
const (
crowdsecLapiHeader = "X-Api-Key"
crowdsecCapiHeader = "Authorization"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiLogin = "v2/watchers/login"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
)
//nolint:gochecknoglobals
var (
isCrowdsecStreamHealthy = false
isCrowdsecStreamHealthy = true
ticker chan bool
)
@@ -51,12 +55,18 @@ type Bouncer struct {
crowdsecHost string
crowdsecKey string
crowdsecMode string
crowdsecMachineID string
crowdsecPassword string
crowdsecScenarios []string
updateInterval int64
defaultDecisionTimeout int64
customHeader string
crowdsecStreamRoute string
crowdsecHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
client *http.Client
httpClient *http.Client
cacheClient *cache.Client
}
// 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)
err := configuration.ValidateParams(config)
if err != nil {
logger.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
logger.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
return nil, err
}
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
tlsConfig, err := configuration.GetTLSConfigCrowdsec(config)
var tlsConfig *tls.Config
crowdsecStreamRoute := ""
crowdsecHeader := ""
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, 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()))
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
}
apiKey = strings.TrimSuffix(apiKey, "\n")
config.CrowdsecLapiKey = apiKey
}
bouncer := &Bouncer{
next: next,
@@ -92,17 +117,22 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
crowdsecMode: config.CrowdsecMode,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: apiKey,
crowdsecKey: config.CrowdsecLapiKey,
crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds,
customHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
serverPoolStrategy: &ip.PoolStrategy{
Checker: serverChecker,
},
clientPoolStrategy: &ip.PoolStrategy{
Checker: clientChecker,
},
client: &http.Client{
httpClient: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
@@ -110,16 +140,24 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
},
Timeout: 10 * time.Second,
},
cacheClient: &cache.Client{},
}
bouncer.cacheClient.New(config.RedisCacheEnabled, config.RedisCacheHost)
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && 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
}
if config.RedisCacheEnabled {
cache.InitRedisClient(config.RedisCacheHost)
}
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
ticker = startTicker(config, func() {
handleStreamCache(bouncer)
})
go handleStreamCache(bouncer)
}
logger.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
return bouncer, nil
}
@@ -155,9 +193,9 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// TODO This should be simplified
if bouncer.crowdsecMode != configuration.NoneMode {
isBanned, erro := cache.GetDecision(remoteIP)
isBanned, erro := bouncer.cacheClient.GetDecision(remoteIP)
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 {
rw.WriteHeader(http.StatusForbidden)
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.
if bouncer.crowdsecMode == configuration.StreamMode {
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
if isCrowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req)
} else {
logger.Error(fmt.Sprintf("ServeHTTP:isCrowdsecStreamHealthy ip:%s", remoteIP))
logger.Error(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
rw.WriteHeader(http.StatusForbidden)
}
} else {
err = handleNoStreamCache(bouncer, remoteIP)
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)
} else {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP))
bouncer.next.ServeHTTP(rw, req)
}
}
@@ -213,6 +252,13 @@ type Stream struct {
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 {
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
stop := make(chan bool, 1)
@@ -239,14 +285,14 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
Path: crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
}
body, err := crowdsecQuery(bouncer, routeURL.String())
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
if err != nil {
return err
}
if bytes.Equal(body, []byte("null")) {
if isLiveMode {
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
}
return nil
}
@@ -258,7 +304,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
}
if len(decisions) == 0 {
if isLiveMode {
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
}
return nil
}
@@ -267,29 +313,53 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
}
if isLiveMode {
cache.SetDecision(remoteIP, true, int64(duration.Seconds()))
bouncer.cacheClient.SetDecision(remoteIP, true, int64(duration.Seconds()))
}
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) {
// TODO clean properly on exit.
// Instead of blocking the goroutine interval for all the secondary node,
// if the master service is shut down, other goroutine can take the lead
// because updated routine information is in the cache
_, err := cache.GetDecision(cacheTimeoutKey)
_, err := bouncer.cacheClient.GetDecision(cacheTimeoutKey)
if err == nil {
logger.Debug("handleStreamCache:alreadyUpdated")
return
}
cache.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
bouncer.cacheClient.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
streamRouteURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiStreamRoute,
Path: bouncer.crowdsecStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
}
body, err := crowdsecQuery(bouncer, streamRouteURL.String())
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
if err != nil {
logger.Error(err.Error())
isCrowdsecStreamHealthy = false
@@ -305,23 +375,40 @@ func handleStreamCache(bouncer *Bouncer) {
for _, decision := range stream.New {
duration, err := time.ParseDuration(decision.Duration)
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 {
cache.DeleteDecision(decision.Value)
bouncer.cacheClient.DeleteDecision(decision.Value)
}
logger.Debug("handleStreamCache:updated")
isCrowdsecStreamHealthy = true
}
func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, error) {
var req *http.Request
if isPost {
data := []byte(fmt.Sprintf(
`{"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(crowdsecLapiHeader, bouncer.crowdsecKey)
res, err := bouncer.client.Do(req)
}
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
res, err := bouncer.httpClient.Do(req)
if err != nil {
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 {
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
}
+7 -3
View File
@@ -8,6 +8,7 @@ import (
"testing"
"text/template"
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
)
@@ -77,7 +78,8 @@ func TestBouncer_ServeHTTP(t *testing.T) {
customHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
client *http.Client
httpClient *http.Client
cacheClient *cache.Client
}
type args struct {
rw http.ResponseWriter
@@ -106,7 +108,8 @@ func TestBouncer_ServeHTTP(t *testing.T) {
customHeader: tt.fields.customHeader,
clientPoolStrategy: tt.fields.clientPoolStrategy,
serverPoolStrategy: tt.fields.serverPoolStrategy,
client: tt.fields.client,
httpClient: tt.fields.httpClient,
cacheClient: tt.fields.cacheClient,
}
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
})
@@ -155,6 +158,7 @@ func Test_crowdsecQuery(t *testing.T) {
type args struct {
bouncer *Bouncer
stringURL string
isPost bool
}
tests := []struct {
name string
@@ -166,7 +170,7 @@ func Test_crowdsecQuery(t *testing.T) {
}
for _, tt := range tests {
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 {
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
return
+1 -1
View File
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.4"
image: "traefik:v2.9.6"
container_name: "traefik"
restart: unless-stopped
command:
+2 -2
View File
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.4"
image: "traefik:v2.9.6"
container_name: "traefik"
restart: unless-stopped
command:
@@ -14,7 +14,7 @@ services:
- "--entrypoints.web.address=:80"
- "--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:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "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
```
+39 -37
View File
@@ -18,14 +18,13 @@ const (
//nolint:gochecknoglobals
var (
cache = ttl_map.New()
redis simpleredis.SimpleRedis
redisEnabled = false
cache = ttl_map.New()
)
// FileSystem Cache
type localCache struct{}
func getDecisionLocalCache(clientIP string) (bool, error) {
func (localCache) getDecision(clientIP string) (bool, error) {
banned, isCached := cache.Get(clientIP)
bannedString, isValid := banned.(string)
if isCached && isValid && len(bannedString) > 0 {
@@ -34,17 +33,17 @@ func getDecisionLocalCache(clientIP string) (bool, error) {
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)
}
func deleteDecisionLocalCache(clientIP string) {
func (localCache) deleteDecision(clientIP string) {
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)
bannedString := string(banned)
if err == nil && len(bannedString) > 0 {
@@ -53,58 +52,61 @@ func getDecisionRedisCache(clientIP string) (bool, error) {
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 {
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 {
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
}
}
// DeleteDecision delete decision in cache.
func DeleteDecision(clientIP string) {
logger.Debug("cache:DeleteDecision")
if redisEnabled {
deleteDecisionRedisCache(clientIP)
type cacheInterface interface {
setDecision(clientIP string, value string, duration int64)
getDecision(clientIP string) (bool, error)
deleteDecision(clientIP string)
}
// 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 {
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.
// Otherwise return with an error to add the IP in cache if we are on.
func GetDecision(clientIP string) (bool, error) {
logger.Debug("cache:GetDecision")
if redisEnabled {
return getDecisionRedisCache(clientIP)
}
return getDecisionLocalCache(clientIP)
func (client *Client) GetDecision(clientIP string) (bool, error) {
logger.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
return client.cache.getDecision(clientIP)
}
// 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
if isBanned {
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v banned", clientIP))
value = cacheBannedValue
} else {
value = cacheNoBannedValue
}
logger.Debug("cache:SetDecision")
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")
client.cache.setDecision(clientIP, value, duration)
}
+38 -162
View File
@@ -6,10 +6,11 @@ import (
"testing"
)
func Test_getDecisionLocalCache(t *testing.T) {
func Test_GetDecision(t *testing.T) {
IPInCache := "10.0.0.10"
IPNotInCache := "10.0.0.20"
setDecisionLocalCache(IPInCache, "t", 10)
client := &Client{cache: &localCache{}}
client.SetDecision(IPInCache, true, 10)
type args struct {
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 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"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getDecisionLocalCache(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)
got, err := client.GetDecision(tt.args.clientIP)
if (err != nil) != tt.wantErr {
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
return
}
if 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 {
clientIP string
isBanned bool
value bool
duration int64
}
tests := []struct {
name string
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 {
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 {
host string
clientIP string
}
tests := []struct {
name string
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 {
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
}
})
}
}
+29 -13
View File
@@ -19,6 +19,7 @@ import (
// Enums for crowdsec mode.
const (
AloneMode = "alone"
StreamMode = "stream"
LiveMode = "live"
NoneMode = "none"
@@ -42,6 +43,11 @@ type Config struct {
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,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"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,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)
}
value = string(fileValue)
return value, nil
return strings.TrimSpace(value), nil
}
field = object.FieldByName(key)
value = field.String()
return value, nil
return strings.TrimSpace(value), nil
}
// ValidateParams validate all the param gave by user.
@@ -115,6 +121,23 @@ func ValidateParams(config *Config) error {
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
testURL := url.URL{
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)
}
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")
if err != nil {
return err
@@ -222,8 +238,8 @@ func validateParamsRequired(config *Config) error {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
if !contains([]string{NoneMode, LiveMode, StreamMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream' or 'alone'")
}
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
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()
//nolint:gocritic
if config.CrowdsecLapiScheme != HTTPS {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme not https")
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
return tlsConfig, nil
} else if config.CrowdsecLapiTLSInsecureVerify {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify is true")
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
tlsConfig.InsecureSkipVerify = true
// If we return here and still want to use client auth this won't work
// return tlsConfig, nil