Compare commits

...
5 Commits
Author SHA1 Message Date
maxlerebourg 81ffeabcec Merge pull request #25 from maxlerebourg/prepare-for-release-1.1.0
Update exemple traefik to version 2.9.1 and version of plugin 1.1.0 in exemples
2022-10-17 21:03:28 +02:00
MathieuHa e14d179612 Update exemple traefik to version 2.9.1 and version of plugin 1.1.0 2022-10-17 21:01:32 +02:00
maxlerebourgandMathieuHa 8696501f61 add one instance of cache for all the service (#19)
*  add one instance of cache for all the service

* 🐛 fix the healthy issue

* 🐛 fix lint

* 🍱 upgrade codebase

* 🍱 fix lint

* 🐛 when there is master node shut down

* Set live mode as default, clean docker and recipes

* Restore live mode in readme as default

* 🍱 add channel management

* 🍱 order funcs

* 🍱 revert service in docker compose

*  logLevel added + cache put out the bouncer

* 🍱 fix

* 🍱 fix

* 🍱 fix

Co-authored-by: MathieuHa <mathieu@hanotaux.fr>
2022-10-16 20:14:41 +02:00
maxlerebourg f22fc2cd09 Merge pull request #22 from maxlerebourg/21-allow-the-real-ip-the-be-fetched-from-a-custom-header
 Add Custom header usage to find the user IP
2022-10-15 21:03:46 +02:00
MathieuHa 8eec1c5656 Add Custom header usage to find the user IP 2022-10-15 18:49:41 +02:00
9 changed files with 206 additions and 114 deletions
+12 -3
View File
@@ -31,15 +31,24 @@ run_behindproxy:
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.
+86 -92
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. return stop
// 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) {
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
+3 -2
View File
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.8.8" image: "traefik:v2.9.1"
container_name: "traefik" container_name: "traefik"
command: command:
# - "--log.level=DEBUG" # - "--log.level=DEBUG"
@@ -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
+2 -2
View File
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.8.8" image: "traefik:v2.9.1"
container_name: "traefik" container_name: "traefik"
command: command:
- "--accesslog" - "--accesslog"
@@ -13,7 +13,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.0.9" - "--experimental.plugins.bouncer.version=v1.1.0"
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"
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
cloudflare: cloudflare:
image: "traefik:v2.8.8" image: "traefik:v2.9.1"
container_name: "cloudflare" container_name: "cloudflare"
command: command:
# - "--log.level=DEBUG" # - "--log.level=DEBUG"
@@ -21,7 +21,7 @@ services:
- 8080:8080 - 8080:8080
traefik: traefik:
image: "traefik:v2.8.8" image: "traefik:v2.9.1"
container_name: "traefik" container_name: "traefik"
command: command:
# - "--log.level=DEBUG" # - "--log.level=DEBUG"
@@ -34,7 +34,7 @@ services:
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5" - "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
- "--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.0.9" - "--experimental.plugins.bouncer.version=v1.1.0"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- logs-dev:/var/log/traefik - logs-dev:/var/log/traefik
+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)
}
+17 -11
View File
@@ -8,7 +8,7 @@ import (
"strings" "strings"
) )
//CHECKER // CHECKER
// Checker allows to check that addresses are in a trusted IPs. // Checker allows to check that addresses are in a trusted IPs.
type Checker struct { type Checker struct {
@@ -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)
}