Merge remote-tracking branch 'origin' into 20-implement-shared-cache-with-an-external-redis-as-an-option

This commit is contained in:
MathieuHa
2022-10-16 22:07:01 +02:00
7 changed files with 200 additions and 108 deletions
+12 -3
View File
@@ -34,15 +34,24 @@ run_cacheredis:
run: run:
docker-compose -f docker-compose.yml up -d --remove-orphans docker-compose -f docker-compose.yml up -d --remove-orphans
restart_docker_dev: restart_dev:
docker-compose -f docker-compose.dev.yml restart docker-compose -f docker-compose.dev.yml restart
restart_docker_local: restart_local:
docker-compose -f docker-compose.local.yml restart docker-compose -f docker-compose.local.yml restart
restart_docker: restart:
docker-compose -f docker-compose.yml restart docker-compose -f docker-compose.yml restart
show_logs:
docker-compose -f docker-compose.yml restart
show_local_logs:
docker-compose -f docker-compose.local.yml logs -f
show_dev_logs:
docker-compose -f docker-compose.dev.yml logs -f
clean_all_docker: clean_all_docker:
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
docker-compose -f docker-compose.local.yml down --remove-orphans docker-compose -f docker-compose.local.yml down --remove-orphans
+9 -1
View File
@@ -49,6 +49,9 @@ At each start of synchronisation, the middleware will wait a random number of se
- Enabled - Enabled
- bool - bool
- enable the plugin - enable the plugin
- LogLevel
- string
- default: `INFO`, expected value are: `INFO`, `DEBUG`
- CrowdsecMode - CrowdsecMode
- string - string
- default: `live`, expected value are: `none`, `live`, `stream` - default: `live`, expected value are: `none`, `live`, `stream`
@@ -74,6 +77,10 @@ At each start of synchronisation, the middleware will wait a random number of se
- []string - []string
- default: [] - default: []
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare) - List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
- ForwardedHeadersCustomName
- string
- default: X-Forwarded-For
- Name of the header where the real IP of the client should be retrieved
### Configuration ### Configuration
@@ -116,13 +123,14 @@ http:
enabled: false enabled: false
updateIntervalSeconds: 60 updateIntervalSeconds: 60
defaultDecisionSeconds: 60 defaultDecisionSeconds: 60
crowdsecMode: stream crowdsecMode: live
crowdsecLapiKey: privateKey crowdsecLapiKey: privateKey
crowdsecLapiHost: crowdsec:8080 crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http crowdsecLapiScheme: http
forwardedHeadersTrustedIPs: forwardedHeadersTrustedIPs:
- 10.0.10.23/32 - 10.0.10.23/32
- 10.0.20.0/24 - 10.0.20.0/24
forwardedHeadersCustomName: X-Custom-Header
``` ```
These are the default values of the plugin except for LapiKey. These are the default values of the plugin except for LapiKey.
+84 -90
View File
@@ -7,16 +7,14 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"math/rand"
"net"
"net/http" "net/http"
"net/url" "net/url"
"text/template" "text/template"
"time" "time"
ttl_map "github.com/leprosus/golang-ttl-map" cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/ip" ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
) )
const ( const (
@@ -26,17 +24,23 @@ const (
crowdsecLapiHeader = "X-Api-Key" crowdsecLapiHeader = "X-Api-Key"
crowdsecLapiRoute = "v1/decisions" crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream" crowdsecLapiStreamRoute = "v1/decisions/stream"
cacheBannedValue = "t" cacheTimeoutKey = "updated"
cacheNoBannedValue = "f" )
var (
crowdsecStreamHealthy = false
ticker chan bool
) )
// Config the plugin configuration. // Config the plugin configuration.
type Config struct { type Config struct {
Enabled bool `json:"enabled,omitempty"` Enabled bool `json:"enabled,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
CrowdsecMode string `json:"crowdsecMode,omitempty"` CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"` CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"` CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"` CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"` UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedheaderstrustedips,omitempty"` ForwardedHeadersTrustedIPs []string `json:"forwardedheaderstrustedips,omitempty"`
@@ -46,6 +50,7 @@ type Config struct {
func CreateConfig() *Config { func CreateConfig() *Config {
return &Config{ return &Config{
Enabled: false, Enabled: false,
LogLevel: "INFO",
CrowdsecMode: liveMode, CrowdsecMode: liveMode,
CrowdsecLapiScheme: "http", CrowdsecLapiScheme: "http",
CrowdsecLapiHost: "crowdsec:8080", CrowdsecLapiHost: "crowdsec:8080",
@@ -53,32 +58,34 @@ func CreateConfig() *Config {
UpdateIntervalSeconds: 60, UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60, DefaultDecisionSeconds: 60,
ForwardedHeadersTrustedIPs: []string{}, ForwardedHeadersTrustedIPs: []string{},
ForwardedHeadersCustomName: "X-Forwarded-For",
} }
} }
// Bouncer a Bouncer plugin. // Bouncer a Bouncer struct.
type Bouncer struct { type Bouncer struct {
next http.Handler next http.Handler
name string name string
template *template.Template template *template.Template
enabled bool enabled bool
crowdsecStreamHealthy bool
crowdsecScheme string crowdsecScheme string
crowdsecHost string crowdsecHost string
crowdsecKey string crowdsecKey string
crowdsecMode string crowdsecMode string
updateInterval int64 updateInterval int64
defaultDecisionTimeout int64 defaultDecisionTimeout int64
customHeader string
poolStrategy *ip.PoolStrategy poolStrategy *ip.PoolStrategy
client *http.Client client *http.Client
cache *ttl_map.Heap
} }
// New creates the crowdsec bouncer plugin. // New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
logger.Init(config.LogLevel)
err := validateParams(config) err := validateParams(config)
if err != nil { if err != nil {
logger.Info(fmt.Sprintf("%w", err))
return nil, err return nil, err
} }
@@ -90,12 +97,12 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
template: template.New("CrowdsecBouncer").Delims("[[", "]]"), template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
enabled: config.Enabled, enabled: config.Enabled,
crowdsecStreamHealthy: false,
crowdsecMode: config.CrowdsecMode, crowdsecMode: config.CrowdsecMode,
crowdsecScheme: config.CrowdsecLapiScheme, crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost, crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey, crowdsecKey: config.CrowdsecLapiKey,
updateInterval: config.UpdateIntervalSeconds, updateInterval: config.UpdateIntervalSeconds,
customHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds, defaultDecisionTimeout: config.DefaultDecisionSeconds,
poolStrategy: &ip.PoolStrategy{ poolStrategy: &ip.PoolStrategy{
Checker: checker, Checker: checker,
@@ -107,18 +114,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
}, },
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
}, },
cache: ttl_map.New(),
} }
if config.CrowdsecMode == streamMode { if config.CrowdsecMode == streamMode {
go func() { go func() {
rand.Seed(time.Now().UnixNano()) if ticker == nil {
timeout := rand.Int63n(30)
logger(fmt.Sprintf("Wait: %v", timeout))
time.Sleep(time.Duration(timeout) * time.Second)
go handleStreamCache(bouncer)
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
for range ticker.C {
go handleStreamCache(bouncer) go handleStreamCache(bouncer)
ticker = startTicker(config, func() {
handleStreamCache(bouncer)
})
} }
}() }()
} }
@@ -132,15 +135,18 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return return
} }
remoteHost, err := getRemoteIP(bouncer, req) remoteHost, err := ip.GetRemoteIP(req, bouncer.poolStrategy, bouncer.customHeader)
if err != nil { if err != nil {
logger.Info(fmt.Sprintf("%w", err))
bouncer.next.ServeHTTP(rw, req) bouncer.next.ServeHTTP(rw, req)
return return
} }
logger.Debug(fmt.Sprintf("ServeHTTP ip:%v", remoteHost))
if bouncer.crowdsecMode != noneMode { if bouncer.crowdsecMode != noneMode {
isBanned, err := getDecision(bouncer.cache, remoteHost) isBanned, err := cache.GetDecision(remoteHost)
if err == nil { if err == nil {
logger.Debug(fmt.Sprintf("ServeHTTP cacheHit isBanned:%v", isBanned))
if isBanned { if isBanned {
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
} else { } else {
@@ -152,7 +158,7 @@ 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 == streamMode { if bouncer.crowdsecMode == streamMode {
if bouncer.crowdsecStreamHealthy { if crowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req) bouncer.next.ServeHTTP(rw, req)
} else { } else {
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
@@ -183,10 +189,6 @@ type Stream struct {
New []Decision `json:"new"` New []Decision `json:"new"`
} }
func logger(str string) {
log.Printf("Crowdsec Bouncer Traefik Plugin - %s", str)
}
func contains(source []string, target string) bool { func contains(source []string, target string) bool {
for _, item := range source { for _, item := range source {
if item == target { if item == target {
@@ -196,42 +198,21 @@ func contains(source []string, target string) bool {
return false return false
} }
// It returns the first IP that is not in the pool, or the empty string otherwise. func startTicker(config *Config, work func()) chan bool {
func getRemoteIP(bouncer *Bouncer, req *http.Request) (string, error) { ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
remoteIP := bouncer.poolStrategy.GetIP(req) stop := make(chan bool, 1)
if len(remoteIP) != 0 { go func() {
return remoteIP, nil defer logger.Debug("ticker:stopped")
} for {
remoteIP, _, err := net.SplitHostPort(req.RemoteAddr) select {
if err != nil { case <-ticker.C:
logger(fmt.Sprintf("failed to extract ip from remote address: %v", err)) go work()
return "", err case <-stop:
} return
return remoteIP, nil
}
// Get Decision 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(cache *ttl_map.Heap, clientIP string) (bool, error) {
banned, isCached := cache.Get(clientIP)
bannedString, isValid := banned.(string)
if isCached && isValid && len(bannedString) > 0 {
return bannedString == cacheBannedValue, nil
}
return false, fmt.Errorf("no cache data")
}
func setDecision(cache *ttl_map.Heap, clientIP string, isBanned bool, duration int64) {
if isBanned {
logger(fmt.Sprintf("%v banned", clientIP))
cache.Set(clientIP, cacheBannedValue, duration)
} else {
cache.Set(clientIP, cacheNoBannedValue, duration)
} }
} }
}()
func deleteDecision(cache *ttl_map.Heap, clientIP string) { return stop
cache.Del(clientIP)
} }
// We are now in none or live mode. // We are now in none or live mode.
@@ -242,26 +223,31 @@ func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Req
Path: crowdsecLapiRoute, Path: crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost), RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost),
} }
body := crowdsecQuery(bouncer, routeURL.String()) body, err := crowdsecQuery(bouncer, routeURL.String())
if err != nil {
logger.Info(fmt.Sprintf("%w", err))
rw.WriteHeader(http.StatusForbidden)
return
}
if bytes.Equal(body, []byte("null")) { if bytes.Equal(body, []byte("null")) {
if bouncer.crowdsecMode == liveMode { if bouncer.crowdsecMode == liveMode {
setDecision(bouncer.cache, remoteHost, false, bouncer.defaultDecisionTimeout) cache.SetDecision(remoteHost, false, bouncer.defaultDecisionTimeout)
} }
bouncer.next.ServeHTTP(rw, req) bouncer.next.ServeHTTP(rw, req)
return return
} }
var decisions []Decision var decisions []Decision
err := json.Unmarshal(body, &decisions) err = json.Unmarshal(body, &decisions)
if err != nil { if err != nil {
logger(fmt.Sprintf("failed to parse body: %s", err)) logger.Info(fmt.Sprintf("failed to parse body: %s", err))
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
return return
} }
if len(decisions) == 0 { if len(decisions) == 0 {
if bouncer.crowdsecMode == liveMode { if bouncer.crowdsecMode == liveMode {
setDecision(bouncer.cache, remoteHost, false, bouncer.defaultDecisionTimeout) cache.SetDecision(remoteHost, false, bouncer.defaultDecisionTimeout)
} }
bouncer.next.ServeHTTP(rw, req) bouncer.next.ServeHTTP(rw, req)
return return
@@ -269,70 +255,78 @@ func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Req
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
duration, err := time.ParseDuration(decisions[0].Duration) duration, err := time.ParseDuration(decisions[0].Duration)
if err != nil { if err != nil {
logger(fmt.Sprintf("failed to parse duration: %s", err)) logger.Info(fmt.Sprintf("failed to parse duration: %s", err))
return return
} }
if bouncer.crowdsecMode == liveMode { if bouncer.crowdsecMode == liveMode {
setDecision(bouncer.cache, remoteHost, true, int64(duration.Seconds())) cache.SetDecision(remoteHost, true, int64(duration.Seconds()))
} }
} }
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,
// if the master service is shut down, other goroutine can take the lead
// because updated routine information is in the cache
logger.Debug("handleStreamCache")
_, err := cache.GetDecision(cacheTimeoutKey)
if err == nil {
return
}
cache.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: crowdsecLapiStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !bouncer.crowdsecStreamHealthy), RawQuery: fmt.Sprintf("startup=%t", !crowdsecStreamHealthy),
} }
body := crowdsecQuery(bouncer, streamRouteURL.String()) body, err := crowdsecQuery(bouncer, streamRouteURL.String())
var stream Stream
err := json.Unmarshal(body, &stream)
if err != nil { if err != nil {
logger(fmt.Sprintf("error while parsing body: %s", err)) logger.Info(fmt.Sprintf("%w", err))
bouncer.crowdsecStreamHealthy = false crowdsecStreamHealthy = false
return
}
var stream Stream
err = json.Unmarshal(body, &stream)
if err != nil {
logger.Info(fmt.Sprintf("error while parsing body: %s", err))
crowdsecStreamHealthy = false
return return
} }
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 {
setDecision(bouncer.cache, decision.Value, true, int64(duration.Seconds())) cache.SetDecision(decision.Value, true, int64(duration.Seconds()))
} }
} }
for _, decision := range stream.Deleted { for _, decision := range stream.Deleted {
deleteDecision(bouncer.cache, decision.Value) cache.DeleteDecision(decision.Value)
} }
bouncer.crowdsecStreamHealthy = true crowdsecStreamHealthy = true
} }
func crowdsecQuery(bouncer *Bouncer, stringURL string) []byte { func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
var req *http.Request var req *http.Request
req, _ = http.NewRequest(http.MethodGet, stringURL, nil) req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
req.Header.Add(crowdsecLapiHeader, bouncer.crowdsecKey) req.Header.Add(crowdsecLapiHeader, bouncer.crowdsecKey)
res, err := bouncer.client.Do(req) res, err := bouncer.client.Do(req)
if err != nil { if err != nil {
logger(fmt.Sprintf("error while fetching %v: %s", stringURL, err)) return nil, fmt.Errorf("error while fetching %v: %s", stringURL, err)
bouncer.crowdsecStreamHealthy = false
return nil
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
logger(fmt.Sprintf("error while fetching %v, status code: %d", stringURL, res.StatusCode)) return nil, fmt.Errorf("error while fetching %v, status code: %d", stringURL, res.StatusCode)
bouncer.crowdsecStreamHealthy = false
return nil
} }
defer func(body io.ReadCloser) { defer func(body io.ReadCloser) {
err = body.Close() err = body.Close()
if err != nil { if err != nil {
logger(fmt.Sprintf("failed to close body reader: %s", err)) logger.Info(fmt.Sprintf("failed to close body reader: %s", err))
} }
}(res.Body) }(res.Body)
body, err := ioutil.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
logger(fmt.Sprintf("error while reading body: %s", err)) return nil, fmt.Errorf("error while reading body: %s", err)
bouncer.crowdsecStreamHealthy = false
return nil
} }
return body return body, nil
} }
func validateParams(config *Config) error { func validateParams(config *Config) error {
@@ -373,10 +367,10 @@ func validateParams(config *Config) error {
if len(config.ForwardedHeadersTrustedIPs) > 0 { if len(config.ForwardedHeadersTrustedIPs) > 0 {
_, err = ip.NewChecker(config.ForwardedHeadersTrustedIPs) _, err = ip.NewChecker(config.ForwardedHeadersTrustedIPs)
if err != nil { if err != nil {
return fmt.Errorf("ForwardedHeadersTrustedIPs must be a list of IP/CIDR :%v", err) return fmt.Errorf("ForwardedHeadersTrustedIPs must be a list of IP/CIDR :%w", err)
} }
} else { } else {
logger("No IP provided for ForwardedHeadersTrustedIPs") logger.Debug("No IP provided for ForwardedHeadersTrustedIPs")
} }
return nil return nil
+2 -1
View File
@@ -35,7 +35,7 @@ services:
- "traefik.http.services.service1.loadbalancer.server.port=80" - "traefik.http.services.service1.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true" - "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" - "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.26.0.1" - "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.26.0.5"
whoami2: whoami2:
image: traefik/whoami image: traefik/whoami
@@ -48,6 +48,7 @@ services:
- "traefik.http.services.service2.loadbalancer.server.port=80" - "traefik.http.services.service2.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true" - "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7" - "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.forwardedheaderstrustedips=172.26.0.5"
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.4.1 image: crowdsecurity/crowdsec:v1.4.1
+39
View File
@@ -0,0 +1,39 @@
package cache
import (
"fmt"
ttl_map "github.com/leprosus/golang-ttl-map"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
const (
cacheBannedValue = "t"
cacheNoBannedValue = "f"
)
var cache = ttl_map.New()
// Get Decision 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) {
banned, isCached := cache.Get(clientIP)
bannedString, isValid := banned.(string)
if isCached && isValid && len(bannedString) > 0 {
return bannedString == cacheBannedValue, nil
}
return false, fmt.Errorf("no cache data")
}
func SetDecision(clientIP string, isBanned bool, duration int64) {
if isBanned {
logger.Debug(fmt.Sprintf("%v banned", clientIP))
cache.Set(clientIP, cacheBannedValue, duration)
} else {
cache.Set(clientIP, cacheNoBannedValue, duration)
}
}
func DeleteDecision(clientIP string) {
cache.Del(clientIP)
}
+16 -10
View File
@@ -80,16 +80,8 @@ func parseIP(addr string) (net.IP, error) {
return userIP, nil return userIP, nil
} }
// STRATEGY // STRATEGY
const (
xForwardedFor = "X-Forwarded-For"
)
// PoolStrategy is a strategy based on an IP Checker. // PoolStrategy is a strategy based on an IP Checker.
// It allows to check whether addresses are in a given pool of IPs. // It allows to check whether addresses are in a given pool of IPs.
type PoolStrategy struct { type PoolStrategy struct {
@@ -99,12 +91,13 @@ type PoolStrategy struct {
// GetIP checks the list of Forwarded IPs (most recent first) against the // GetIP checks the list of Forwarded IPs (most recent first) against the
// Checker pool of IPs. It returns the first IP that is not in the pool, or the // Checker pool of IPs. It returns the first IP that is not in the pool, or the
// empty string otherwise. // empty string otherwise.
func (s *PoolStrategy) GetIP(req *http.Request) string { func (s *PoolStrategy) getIP(req *http.Request, customHeader string) string {
if s.Checker == nil { if s.Checker == nil {
return "" return ""
} }
xff := req.Header.Get(xForwardedFor) xff := req.Header.Get(customHeader)
xffs := strings.Split(xff, ",") xffs := strings.Split(xff, ",")
for i := len(xffs) - 1; i >= 0; i-- { for i := len(xffs) - 1; i >= 0; i-- {
@@ -119,3 +112,16 @@ func (s *PoolStrategy) GetIP(req *http.Request) string {
return "" return ""
} }
// GetRemoteIP It returns the first IP that is not in the pool, or the empty string otherwise.
func GetRemoteIP(req *http.Request, strategy *PoolStrategy, customHeader string) (string, error) {
remoteIP := strategy.getIP(req, customHeader)
if len(remoteIP) != 0 {
return remoteIP, nil
}
remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return "", fmt.Errorf("failed to extract ip from remote address: %w", err)
}
return remoteIP, nil
}
+35
View File
@@ -0,0 +1,35 @@
package logger
import (
"io"
"log"
"os"
)
var (
loggerInfo = log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
loggerDebug = log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
)
// Init Set Default log level to info in case log level to defined
func Init(logLevel string) {
switch logLevel {
case "INFO":
loggerInfo.SetOutput(os.Stdout)
case "DEBUG":
loggerInfo.SetOutput(os.Stdout)
loggerDebug.SetOutput(os.Stdout)
default:
loggerInfo.SetOutput(os.Stdout)
}
}
// Info Log info
func Info(str string) {
loggerInfo.Printf(str)
}
// Info Log debug
func Debug(str string) {
loggerDebug.Printf(str)
}