mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ffeabcec | ||
|
|
e14d179612 | ||
|
|
8696501f61 | ||
|
|
f22fc2cd09 | ||
|
|
8eec1c5656 | ||
|
|
552b30a9ef |
@@ -25,18 +25,35 @@ run_dev:
|
|||||||
run_local:
|
run_local:
|
||||||
docker-compose -f docker-compose.local.yml up -d --remove-orphans
|
docker-compose -f docker-compose.local.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_behindproxy:
|
||||||
|
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans
|
||||||
|
|
||||||
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:
|
||||||
|
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.yml down --remove-orphans
|
||||||
|
|
||||||
show_metrics:
|
show_metrics:
|
||||||
docker exec crowdsec cscli metrics
|
docker exec crowdsec cscli metrics
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
@@ -70,6 +73,14 @@ At each start of synchronisation, the middleware will wait a random number of se
|
|||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
- Used only in `live` mode, decision duration of accepted IPs
|
- Used only in `live` mode, decision duration of accepted IPs
|
||||||
|
- ForwardedHeadersTrustedIPs
|
||||||
|
- []string
|
||||||
|
- default: []
|
||||||
|
- 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
|
||||||
|
|
||||||
@@ -112,10 +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:
|
||||||
|
- 10.0.10.23/32
|
||||||
|
- 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.
|
||||||
|
|
||||||
@@ -185,6 +200,33 @@ For local developpement a docker-compose.local.yml is provided and reproduce the
|
|||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.local.yml up -d
|
docker-compose -f docker-compose.local.yml up -d
|
||||||
```
|
```
|
||||||
|
Equivalent to
|
||||||
|
```bash
|
||||||
|
make run_local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemples
|
||||||
|
|
||||||
|
1. Behind another proxy service (ex: clouflare)
|
||||||
|
|
||||||
|
You need to configure your Traefik to trust Forwarded headers by your front proxy
|
||||||
|
In the exemple we use another instance of traefik with the container named cloudflare to simulate a front proxy
|
||||||
|
|
||||||
|
The "internal" Traefik instance is configured to trust the cloudflare forward headers
|
||||||
|
```yaml
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
We configure the middleware to trust as well the IP:
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the environnement run:
|
||||||
|
```bash
|
||||||
|
make run_behind_proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### About
|
### About
|
||||||
|
|
||||||
|
|||||||
+123
-100
@@ -7,15 +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/pkg/ip"
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,75 +22,91 @@ const (
|
|||||||
liveMode = "live"
|
liveMode = "live"
|
||||||
noneMode = "none"
|
noneMode = "none"
|
||||||
crowdsecLapiHeader = "X-Api-Key"
|
crowdsecLapiHeader = "X-Api-Key"
|
||||||
crowdsecCapiHeader = "Authorization"
|
|
||||||
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"`
|
||||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
|
||||||
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
|
ForwardedHeadersTrustedIPs []string `json:"forwardedheaderstrustedips,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateConfig creates the default plugin configuration.
|
// CreateConfig creates the default plugin configuration.
|
||||||
func CreateConfig() *Config {
|
func CreateConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
CrowdsecMode: liveMode,
|
LogLevel: "INFO",
|
||||||
CrowdsecLapiScheme: "http",
|
CrowdsecMode: liveMode,
|
||||||
CrowdsecLapiHost: "crowdsec:8080",
|
CrowdsecLapiScheme: "http",
|
||||||
CrowdsecLapiKey: "",
|
CrowdsecLapiHost: "crowdsec:8080",
|
||||||
UpdateIntervalSeconds: 60,
|
CrowdsecLapiKey: "",
|
||||||
DefaultDecisionSeconds: 60,
|
UpdateIntervalSeconds: 60,
|
||||||
|
DefaultDecisionSeconds: 60,
|
||||||
|
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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||||
|
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
next: next,
|
next: next,
|
||||||
name: name,
|
name: name,
|
||||||
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{
|
||||||
|
Checker: checker,
|
||||||
|
},
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
@@ -99,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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -124,17 +135,18 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Make sur remote address does not include the port.
|
remoteHost, err := ip.GetRemoteIP(req, bouncer.poolStrategy, bouncer.customHeader)
|
||||||
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger(fmt.Sprintf("failed to extract ip from remote address: %v", err))
|
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 == streamMode || bouncer.crowdsecMode == liveMode {
|
if bouncer.crowdsecMode != noneMode {
|
||||||
isBanned, err := getDecision(bouncer, 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 {
|
||||||
@@ -146,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)
|
||||||
@@ -177,17 +189,6 @@ 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 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 {
|
||||||
@@ -197,122 +198,135 @@ func contains(source []string, target string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Decision check in the cache if the IP has the banned / not banned value.
|
func startTicker(config *Config, work func()) chan bool {
|
||||||
// Otherwise return with an error to add the IP in cache if we are on.
|
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
||||||
func getDecision(bouncer *Bouncer, clientIP string) (bool, error) {
|
stop := make(chan bool, 1)
|
||||||
banned, isCached := bouncer.cache.Get(clientIP)
|
go func() {
|
||||||
bannedString, isValid := banned.(string)
|
defer logger.Debug("ticker:stopped")
|
||||||
if isCached && isValid && len(bannedString) > 0 {
|
for {
|
||||||
return bannedString == cacheBannedValue, nil
|
select {
|
||||||
}
|
case <-ticker.C:
|
||||||
return false, fmt.Errorf("no cache data")
|
go work()
|
||||||
}
|
case <-stop:
|
||||||
|
return
|
||||||
func setDecision(bouncer *Bouncer, clientIP string, isBanned bool, duration int64) {
|
}
|
||||||
if bouncer.crowdsecMode == noneMode {
|
}
|
||||||
return
|
}()
|
||||||
}
|
return stop
|
||||||
if isBanned {
|
|
||||||
logger(fmt.Sprintf("%v banned", clientIP))
|
|
||||||
bouncer.cache.Set(clientIP, cacheBannedValue, duration)
|
|
||||||
} else {
|
|
||||||
bouncer.cache.Set(clientIP, cacheNoBannedValue, duration)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We are now in none or live mode.
|
||||||
func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
|
func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
|
||||||
// We are now in none or live mode.
|
|
||||||
routeURL := url.URL{
|
routeURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
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")) {
|
||||||
setDecision(bouncer, remoteHost, false, bouncer.defaultDecisionTimeout)
|
if bouncer.crowdsecMode == liveMode {
|
||||||
|
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 {
|
||||||
setDecision(bouncer, remoteHost, false, bouncer.defaultDecisionTimeout)
|
if bouncer.crowdsecMode == liveMode {
|
||||||
|
cache.SetDecision(remoteHost, false, bouncer.defaultDecisionTimeout)
|
||||||
|
}
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
setDecision(bouncer, remoteHost, true, int64(duration.Seconds()))
|
if bouncer.crowdsecMode == liveMode {
|
||||||
|
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, decision.Value, true, int64(duration.Seconds()))
|
cache.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, decision := range stream.Deleted {
|
for _, decision := range stream.Deleted {
|
||||||
bouncer.cache.Del(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 {
|
||||||
@@ -350,5 +364,14 @@ func validateParams(config *Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
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 len(config.ForwardedHeadersTrustedIPs) > 0 {
|
||||||
|
_, err = ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ForwardedHeadersTrustedIPs must be a list of IP/CIDR :%w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Debug("No IP provided for ForwardedHeadersTrustedIPs")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -18,13 +18,6 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- logs-local:/var/log/traefik
|
- logs-local:/var/log/traefik
|
||||||
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapilogin=3829a6c9870e4726a377d8951ebb64a1m8psGvMaq1ykJ3zX"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapipwd=Q5pgN8bRNInHGdx6QCksdPOJVLeLQ7ipJntSeuP3r8088zXzRVs4G8liXAKfI1k6"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/http-backdoors-attempts,baudneo/zoneminder-bf"
|
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
@@ -36,31 +29,26 @@ services:
|
|||||||
container_name: "simple-service1"
|
container_name: "simple-service1"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# Definition of the router
|
|
||||||
- "traefik.http.routers.router1.rule=Host(`localhost`) && Path(`/foo`)"
|
- "traefik.http.routers.router1.rule=Host(`localhost`) && Path(`/foo`)"
|
||||||
- "traefik.http.routers.router1.entrypoints=web"
|
- "traefik.http.routers.router1.entrypoints=web"
|
||||||
- "traefik.http.routers.router1.middlewares=crowdsec1@docker"
|
- "traefik.http.routers.router1.middlewares=crowdsec1@docker"
|
||||||
# Definition of the service
|
|
||||||
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
|
||||||
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
|
||||||
- "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.5"
|
||||||
|
|
||||||
whoami2:
|
whoami2:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service2"
|
container_name: "simple-service2"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# Definition of the router
|
|
||||||
- "traefik.http.routers.router2.rule=Host(`localhost`) && Path(`/bar`)"
|
- "traefik.http.routers.router2.rule=Host(`localhost`) && Path(`/bar`)"
|
||||||
- "traefik.http.routers.router2.entrypoints=web"
|
- "traefik.http.routers.router2.entrypoints=web"
|
||||||
- "traefik.http.routers.router2.middlewares=crowdsec2@docker"
|
- "traefik.http.routers.router2.middlewares=crowdsec2@docker"
|
||||||
# Definition of the service
|
|
||||||
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
|
||||||
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
|
||||||
- "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
|
||||||
|
|||||||
+9
-6
@@ -2,10 +2,9 @@ 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"
|
|
||||||
- "--accesslog"
|
- "--accesslog"
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
@@ -14,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.6"
|
- "--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"
|
||||||
@@ -35,10 +34,12 @@ services:
|
|||||||
- "traefik.http.routers.router1.middlewares=crowdsec2@docker"
|
- "traefik.http.routers.router1.middlewares=crowdsec2@docker"
|
||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
# Definition of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
# crowdseclapikey must be unique to the middleware attached to the service
|
||||||
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1"
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1"
|
||||||
|
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
whoami2:
|
whoami2:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
@@ -53,8 +54,10 @@ services:
|
|||||||
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
# Definitin of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
# crowdseclapikey must be unique to the middleware attached to the service
|
||||||
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
|
||||||
|
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
image: crowdsecurity/crowdsec:v1.4.1
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
http:
|
||||||
|
# Add the router
|
||||||
|
routers:
|
||||||
|
router0:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: service-foo
|
||||||
|
rule: Path(`/foo`)
|
||||||
|
|
||||||
|
# Add the service
|
||||||
|
services:
|
||||||
|
service-foo:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://traefik/foo:80
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
cloudflare:
|
||||||
|
image: "traefik:v2.9.1"
|
||||||
|
container_name: "cloudflare"
|
||||||
|
command:
|
||||||
|
# - "--log.level=DEBUG"
|
||||||
|
- "--accesslog"
|
||||||
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--providers.file.filename=/cloud.yaml"
|
||||||
|
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./cloudflare-exemple.yaml:/cloud.yaml:ro
|
||||||
|
- logs-cloudflare:/var/log/traefik
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 8080:8080
|
||||||
|
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v2.9.1"
|
||||||
|
container_name: "traefik"
|
||||||
|
command:
|
||||||
|
# - "--log.level=DEBUG"
|
||||||
|
- "--accesslog"
|
||||||
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
|
||||||
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
- "--experimental.plugins.bouncer.version=v1.1.0"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- logs-dev:/var/log/traefik
|
||||||
|
ports:
|
||||||
|
- 90:80
|
||||||
|
- 9080:8080
|
||||||
|
depends_on:
|
||||||
|
- crowdsec
|
||||||
|
|
||||||
|
whoami1:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service1"
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Definition of the router
|
||||||
|
- "traefik.http.routers.router1.rule=Path(`/foo`)"
|
||||||
|
- "traefik.http.routers.router1.entrypoints=web"
|
||||||
|
- "traefik.http.routers.router1.middlewares=crowdsec1@docker"
|
||||||
|
# Definition of the service
|
||||||
|
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
||||||
|
# Definitin of the middleware
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
||||||
|
# crowdseclapikey must be uniq to the middleware attached to the service
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdsecmode=live"
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
|
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:v1.4.1
|
||||||
|
container_name: "crowdsec"
|
||||||
|
environment:
|
||||||
|
COLLECTIONS: crowdsecurity/traefik
|
||||||
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
|
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
|
||||||
|
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
|
||||||
|
volumes:
|
||||||
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
|
- logs-dev:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-dev:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-dev:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-dev:
|
||||||
|
logs-cloudflare:
|
||||||
|
crowdsec-db-dev:
|
||||||
|
crowdsec-config-dev:
|
||||||
Vendored
+39
@@ -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)
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
package ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CHECKER
|
||||||
|
|
||||||
|
// Checker allows to check that addresses are in a trusted IPs.
|
||||||
|
type Checker struct {
|
||||||
|
authorizedIPs []*net.IP
|
||||||
|
authorizedIPsNet []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs.
|
||||||
|
func NewChecker(trustedIPs []string) (*Checker, error) {
|
||||||
|
if len(trustedIPs) == 0 {
|
||||||
|
return nil, errors.New("no trusted IPs provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
checker := &Checker{}
|
||||||
|
|
||||||
|
for _, ipMask := range trustedIPs {
|
||||||
|
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
||||||
|
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ipAddr, err := net.ParseCIDR(ipMask)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
|
||||||
|
}
|
||||||
|
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return checker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains checks if provided address is in the trusted IPs.
|
||||||
|
func (ip *Checker) Contains(addr string) (bool, error) {
|
||||||
|
if len(addr) == 0 {
|
||||||
|
return false, errors.New("empty IP address")
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddr, err := parseIP(addr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("unable to parse address: %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip.ContainsIP(ipAddr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsIP checks if provided address is in the trusted IPs.
|
||||||
|
func (ip *Checker) ContainsIP(addr net.IP) bool {
|
||||||
|
for _, authorizedIP := range ip.authorizedIPs {
|
||||||
|
if authorizedIP.Equal(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, authorizedNet := range ip.authorizedIPsNet {
|
||||||
|
if authorizedNet.Contains(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIP(addr string) (net.IP, error) {
|
||||||
|
userIP := net.ParseIP(addr)
|
||||||
|
if userIP == nil {
|
||||||
|
return nil, fmt.Errorf("can't parse IP from address %s", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// STRATEGY
|
||||||
|
|
||||||
|
// PoolStrategy is a strategy based on an IP Checker.
|
||||||
|
// It allows to check whether addresses are in a given pool of IPs.
|
||||||
|
type PoolStrategy struct {
|
||||||
|
Checker *Checker
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// empty string otherwise.
|
||||||
|
func (s *PoolStrategy) getIP(req *http.Request, customHeader string) string {
|
||||||
|
if s.Checker == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
xff := req.Header.Get(customHeader)
|
||||||
|
|
||||||
|
xffs := strings.Split(xff, ",")
|
||||||
|
|
||||||
|
for i := len(xffs) - 1; i >= 0; i-- {
|
||||||
|
xffTrimmed := strings.TrimSpace(xffs[i])
|
||||||
|
if len(xffTrimmed) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if contain, _ := s.Checker.Contains(xffTrimmed); !contain {
|
||||||
|
return xffTrimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user