Compare commits

...
27 Commits
Author SHA1 Message Date
maxlerebourg 1baa5d7667 Merge pull request #31 from maxlerebourg/30-significant-slow-down-after-updating-to-v111-and-configuring-to-use-redis
 rewrite redis communication
2022-11-11 00:14:00 +01:00
Max Lerebourg 18f68de196 rewrite redis communication 2022-11-11 00:09:06 +01:00
maxlerebourg 4a5f1eca6a Merge pull request #24 from maxlerebourg/20-implement-shared-cache-with-an-external-redis-as-an-option
Create Environnement to work on redis cache
2022-11-06 13:02:58 +01:00
maxlerebourg cdda369fb8 Merge branch 'main' into 20-implement-shared-cache-with-an-external-redis-as-an-option 2022-11-06 13:02:40 +01:00
Max Lerebourg f0f28fecef 🍱 fix readme 2022-11-06 13:01:25 +01:00
Max Lerebourg 23620207f7 🍱 fix before version 2022-11-06 12:57:32 +01:00
maxlerebourg 4a674da9f9 Merge pull request #29 from maxlerebourg/max-impl-redis
Max impl redis
2022-11-06 12:50:19 +01:00
MathieuHa 0dfd18f18e Update documentation for cache 2022-11-05 21:55:29 +01:00
MathieuHa d8ee0a34eb Update documentation, exemples and code for redis support 2022-11-05 21:45:43 +01:00
maxlerebourg b50074dca4 Merge pull request #26 from maxlerebourg/max-impl-redis
Max impl redis
2022-11-05 20:01:51 +01:00
maxlerebourg 5044004ec2 Update cache.go 2022-10-26 10:26:19 +02:00
Max Lerebourg 41a46c0584 add delete 2022-10-24 21:56:38 +02:00
Max Lerebourg 0a186cf9a9 add redis ttl 2022-10-24 21:38:56 +02:00
Max Lerebourg ffcf4356fc redis included 2022-10-20 02:42:24 +02:00
Max Lerebourg f94e48aa03 not working... 2022-10-19 08:22:00 +02:00
Max Lerebourg a197194591 not working... 2022-10-19 08:21:01 +02:00
Max Lerebourg 44e329cd57 🍱 fix 2022-10-17 22:14:49 +02:00
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
MathieuHa 4058836678 Test library github.com/gomodule/redigo/redis 2022-10-17 20:58:27 +02:00
MathieuHa 87ed9e9c4e add bad library 2022-10-16 23:10:39 +02:00
MathieuHa 395c80dccf First version not working, unsafe library 2022-10-16 23:08:21 +02:00
MathieuHa 59268ee33d Merge remote-tracking branch 'origin' into 20-implement-shared-cache-with-an-external-redis-as-an-option 2022-10-16 22:07:01 +02:00
MathieuHa 781a83465e Create Environnement to work on redis cache 2022-10-16 20:47:29 +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
17 changed files with 1098 additions and 130 deletions
+16 -3
View File
@@ -28,20 +28,33 @@ run_local:
run_behindproxy: run_behindproxy:
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans
run_cacheredis:
docker-compose -f exemples/redis-cache/docker-compose.redis.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: 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 exemples/behind-proxy/docker-compose.redis.yml down --remove-orphans
docker-compose -f docker-compose.local.yml down --remove-orphans docker-compose -f docker-compose.local.yml down --remove-orphans
docker-compose -f docker-compose.yml down --remove-orphans docker-compose -f docker-compose.yml down --remove-orphans
+40 -9
View File
@@ -26,6 +26,9 @@ There are 3 operating modes (CrowdsecMode) for this plugin:
The recommanded mode for performance is the streaming mode, decisions are updated every 60 sec by default and that's the only communication between traefik and crowdsec. Every requests that happens hits the cache for quick decisions. The recommanded mode for performance is the streaming mode, decisions are updated every 60 sec by default and that's the only communication between traefik and crowdsec. Every requests that happens hits the cache for quick decisions.
The cache can be local to the Traefik instance using the filesystem or use of a separated redis instance.
The redis instance is currently in beta and support Redis 7.0.X version
## Usage ## Usage
To get started, use the `docker-compose.yml` file. To get started, use the `docker-compose.yml` file.
@@ -36,19 +39,19 @@ make run
``` ```
### Note ### Note
Each middleware in traefik has it's own data and is instanciated by service.
This means if there are 10 services protected by the bouncer in streaming alone or live mode, the cache will be duplicated to all 10 services.
This is because traefik does not allow plugins to store data locally that can be consummed.
The synchronisation with the crowdsec service will happen also 10 times in the period selected. **/!\ Since Release 1.10, cache is no longer duplicated but shared by all services**
It should be taken into account when fixing this period so each middleware has time to sync data from crowdsec. *This lowers the overhead of the cache in memory and the numbers of cache to fetch it from crowdsec in situation with many services*
At each start of synchronisation, the middleware will wait a random number of seconds to avoid simultaneous calls to crowdsec.
### Variables ### Variables
- Enabled - Enabled
- bool - bool
- enable the plugin - enable the plugin
- default: false
- 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`
@@ -58,7 +61,7 @@ At each start of synchronisation, the middleware will wait a random number of se
- CrowdsecLapiHost - CrowdsecLapiHost
- string - string
- default: "crowdsec:8080" - default: "crowdsec:8080"
- Crowdsec LAPI available on which host. - Crowdsec LAPI available on which host and port.
- CrowdsecLapiKey - CrowdsecLapiKey
- string - string
- Crowdsec LAPI generated key for the bouncer : **must be unique by service**. - Crowdsec LAPI generated key for the bouncer : **must be unique by service**.
@@ -74,6 +77,19 @@ 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
- RedisCacheEnabled
- bool
- default: false
- enable redis cache instead of filesystem cache
- RedisCacheHost
- string
- default: "redis:6379"
- hostname and port for the redis service
### Configuration ### Configuration
@@ -116,13 +132,16 @@ 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
redisCacheEnabled: false
redisCacheHost: "redis:6379"
``` ```
These are the default values of the plugin except for LapiKey. These are the default values of the plugin except for LapiKey.
@@ -216,9 +235,21 @@ We configure the middleware to trust as well the IP:
To run the environnement run: To run the environnement run:
```bash ```bash
make run_behind_proxy make run_behindproxy
``` ```
2. With Redis as an external shared cache
The plugin must be configured to connect to a redis instance
```yaml
redisCacheHost: "redis:6379"
```
Here **redis** is the hostname of a container located in the same network as Traefik and **6379** the default port of redis
To run the demo environnement run:
```bash
make run_cacheredis
```
### About ### About
+93 -95
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,26 +24,35 @@ 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"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,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,
LogLevel: "INFO",
CrowdsecMode: liveMode, CrowdsecMode: liveMode,
CrowdsecLapiScheme: "http", CrowdsecLapiScheme: "http",
CrowdsecLapiHost: "crowdsec:8080", CrowdsecLapiHost: "crowdsec:8080",
@@ -53,32 +60,36 @@ func CreateConfig() *Config {
UpdateIntervalSeconds: 60, UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60, DefaultDecisionSeconds: 60,
ForwardedHeadersTrustedIPs: []string{}, ForwardedHeadersTrustedIPs: []string{},
ForwardedHeadersCustomName: "X-Forwarded-For",
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
} }
} }
// 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 +101,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,21 +118,17 @@ 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.RedisCacheEnabled {
go func() { cache.InitRedisClient(config.RedisCacheHost)
rand.Seed(time.Now().UnixNano()) }
timeout := rand.Int63n(30) if config.CrowdsecMode == streamMode && ticker == nil {
logger(fmt.Sprintf("Wait: %v", timeout)) ticker = startTicker(config, func() {
time.Sleep(time.Duration(timeout) * time.Second) handleStreamCache(bouncer)
go handleStreamCache(bouncer) })
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
for range ticker.C {
go handleStreamCache(bouncer) go handleStreamCache(bouncer)
} }
}()
}
return bouncer, nil return bouncer, nil
} }
@@ -132,15 +139,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 +162,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 +193,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 +202,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 +227,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 +259,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 +371,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
+1 -2
View File
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.8.8" image: "traefik:v2.9.4"
container_name: "traefik" container_name: "traefik"
command: command:
# - "--log.level=DEBUG" # - "--log.level=DEBUG"
@@ -35,7 +35,6 @@ 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"
whoami2: whoami2:
image: traefik/whoami image: traefik/whoami
+4 -4
View File
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.8.8" image: "traefik:v2.9.4"
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"
@@ -29,7 +29,7 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
# Definition of the router # Definition of the router
- "traefik.http.routers.router1.rule=Host(`localhost`) && Path(`/foo`)" - "traefik.http.routers.router1.rule=Path(`/foo`)"
- "traefik.http.routers.router1.entrypoints=web" - "traefik.http.routers.router1.entrypoints=web"
- "traefik.http.routers.router1.middlewares=crowdsec2@docker" - "traefik.http.routers.router1.middlewares=crowdsec2@docker"
# Definition of the service # Definition of the service
@@ -47,7 +47,7 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
# Definition of the router # Definition of the router
- "traefik.http.routers.router2.rule=Host(`localhost`) && Path(`/bar`)" - "traefik.http.routers.router2.rule=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 # Definition of the service
@@ -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
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
@@ -0,0 +1,96 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.4"
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"
#- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
#- "--experimental.plugins.bouncer.version=v1.0.9"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-redis:/var/log/traefik
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
depends_on:
- crowdsec
- redis
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"
# Definition 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.rediscacheenabled=true"
whoami2:
image: 4206969/spiderfoot
container_name: "simple-service2"
volumes:
- ./spiderfoot-data:/var/lib/spiderfoot
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router2.rule=PathPrefix(`/`)"
- "traefik.http.routers.router2.entrypoints=web"
- "traefik.http.routers.router2.middlewares=crowdsec1@docker"
# Definition of the service
- "traefik.http.services.service2.loadbalancer.server.port=5001"
# Definition of the middleware
- "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.rediscacheenabled=true"
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-redis:/var/log/traefik:ro
- crowdsec-db-redis:/var/lib/crowdsec/data/
- crowdsec-config-redis:/etc/crowdsec/
labels:
- "traefik.enable=false"
redis:
image: "redis:7.0.5-alpine"
container_name: "redis"
command: "redis-server --save 60 1"
volumes:
- redis-data:/data
ports:
- 6379:6379
volumes:
logs-redis:
crowdsec-db-redis:
crowdsec-config-redis:
redis-data:
+2
View File
@@ -3,3 +3,5 @@ module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
go 1.17 go 1.17
require github.com/leprosus/golang-ttl-map v1.1.7 require github.com/leprosus/golang-ttl-map v1.1.7
require github.com/tehnerd/goUtils v0.0.0-20150515130609-5a2d8fb2ded8 // indirect
+2
View File
@@ -1,2 +1,4 @@
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM= github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0= github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
github.com/tehnerd/goUtils v0.0.0-20150515130609-5a2d8fb2ded8 h1:/b777evAfRRdUJHasZLgQ/w8D/s1HtbaeDXsCVsV0B0=
github.com/tehnerd/goUtils v0.0.0-20150515130609-5a2d8fb2ded8/go.mod h1:UuuqaOb+pZOxJZtjF1mBWTo8HYa7HQCbNkwUEaG9uU0=
+98
View File
@@ -0,0 +1,98 @@
package cache
import (
"fmt"
ttl_map "github.com/leprosus/golang-ttl-map"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/redis"
)
const (
cacheBannedValue = "t"
cacheNoBannedValue = "f"
)
var cache = ttl_map.New()
var redis simpleredis.SimpleRedis
var redisEnabled = false
// CLASSIC
func getDecisionLocalCache(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 setDecisionLocalCache(clientIP string, value string, duration int64) {
cache.Set(clientIP, value, duration)
}
func deleteDecisionLocalCache(clientIP string) {
cache.Del(clientIP)
}
// REDIS
func getDecisionRedisCache(clientIP string) (bool, error) {
banned, err := redis.Get(clientIP)
bannedString := string(banned)
if err == nil && len(bannedString) > 0 {
return bannedString == cacheBannedValue, nil
}
return false, fmt.Errorf("no cache data")
}
func setDecisionRedisCache(clientIP string, value string, duration int64) {
redis.Set(clientIP, []byte(value), duration)
}
func deleteDecisionRedisCache(clientIP string) {
redis.Del(clientIP)
}
// DeleteDecision delete decision in cache
func DeleteDecision(clientIP string) {
if redisEnabled {
deleteDecisionRedisCache(clientIP)
} else {
deleteDecisionLocalCache(clientIP)
}
}
// GetDecision check in the cache if the IP has the banned / not banned value.
// Otherwise return with an error to add the IP in cache if we are on.
func GetDecision(clientIP string) (bool, error) {
if redisEnabled {
return getDecisionRedisCache(clientIP)
} else {
return getDecisionLocalCache(clientIP)
}
}
func SetDecision(clientIP string, isBanned bool, duration int64) {
var value string
if isBanned {
logger.Debug(fmt.Sprintf("%v banned", clientIP))
value = cacheBannedValue
} else {
value = cacheNoBannedValue
}
if redisEnabled {
setDecisionRedisCache(clientIP, value, duration)
} else {
setDecisionLocalCache(clientIP, value, duration)
}
}
func InitRedisClient(host string) {
redisEnabled = true
redis.Init(host)
logger.Debug("Redis initialized")
}
+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)
}
+114
View File
@@ -0,0 +1,114 @@
package simpleredis
import (
"bufio"
"fmt"
"net"
"net/textproto"
"strings"
"time"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
type RedisCmd struct {
Command string
Name string
Data []byte
Duration int64
Error error
}
type SimpleRedis struct {
redisHost string
}
func genRedisArray(params ...[]byte) []byte {
MSG := ""
for cntr := 0; cntr < len(params); cntr++ {
MSG = strings.Join([]string{MSG, string(params[cntr])}, " ")
}
MSG = strings.Trim(MSG, " ")
MSG = strings.Join([]string{MSG, "\r\n"}, "")
return []byte(MSG)
}
func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
conn, err := net.Dial("tcp", hostnamePort)
if err != nil {
return
}
defer conn.Close()
writer := textproto.NewWriter(bufio.NewWriter(conn))
reader := textproto.NewReader(bufio.NewReader(conn))
switch cmd.Command {
case "SET":
data := genRedisArray([]byte("SET"), []byte(cmd.Name), []byte(cmd.Data), []byte("EX"), []byte(fmt.Sprintf("%v", cmd.Duration)))
writer.PrintfLine(string(data))
logger.Info("set")
case "DEL":
data := genRedisArray([]byte("DEL"), []byte(cmd.Name))
writer.PrintfLine(string(data))
logger.Info("del")
case "GET":
data := genRedisArray([]byte("GET"), []byte(cmd.Name))
writer.PrintfLine(string(data))
logger.Info("get")
for {
select {
case <-time.After(time.Second * 1):
channel <- RedisCmd{Error: fmt.Errorf("timeout")}
return
default:
read, _ := reader.ReadLineBytes()
if string(read) != "$1" {
channel <- RedisCmd{Error: fmt.Errorf("miss")}
return
}
read, _ = reader.ReadLineBytes()
channel <- RedisCmd{Data: read}
return
}
}
}
}
func (sr *SimpleRedis) Init(redisHost string) {
sr.redisHost = redisHost
}
func (sr *SimpleRedis) Get(name string) ([]byte, error) {
redisCmd := RedisCmd{
Command: "GET",
Name: name,
}
channel := make(chan RedisCmd)
go askRedis(sr.redisHost, redisCmd, channel)
resp := <-channel
if resp.Error != nil {
return nil, resp.Error
}
return resp.Data, nil
}
func (sr *SimpleRedis) Set(name string, data []byte, duration int64) error {
redisCmd := RedisCmd{
Command: "SET",
Name: name,
Data: data,
Duration: duration,
}
go askRedis(sr.redisHost, redisCmd, nil)
return nil
}
func (sr *SimpleRedis) Del(name string) error {
redisCmd := RedisCmd{
Command: "DEL",
Name: name,
}
go askRedis(sr.redisHost, redisCmd, nil)
return nil
}
+525
View File
@@ -0,0 +1,525 @@
package netutils
import (
"math/rand"
"net"
"strconv"
"strings"
"sync"
//"sync/atomic"
"time"
)
//Receive msg from tcp socket and send it as a []byte to readChan
func ReadFromTCP2(sock *net.TCPConn, msgBuf []byte, readChan chan []byte,
feedbackChanFromSocket chan int) {
loop := 1
for loop == 1 {
bytes, err := sock.Read(msgBuf)
if err != nil {
feedbackChanFromSocket <- 1
loop = 0
continue
}
b := make([]byte, 0)
b = append(b, msgBuf[:bytes]...)
readChan <- b
}
}
//Receive msg from tcp socket and send it as a []byte to readChan
func ReadFromTCP(sock *net.TCPConn, msgBuf []byte, readChan chan []byte,
feedbackChanFromSocket chan int) {
feedbackToSocket := make(chan bool)
feedbackFromSocket := make(chan bool)
reuseBufferChan := make(chan []byte, 1)
go readFromTCP(sock, readChan, feedbackFromSocket,
feedbackToSocket, reuseBufferChan)
go func() {
<-feedbackFromSocket
feedbackChanFromSocket <- 1
}()
}
/*simple write to TCP, for oneway connections only (no communitcation w/ "read" part of the socket
in terms of error propogation)*/
func WriteToTCPw2(sock *net.TCPConn, writeChan chan []byte,
feedbackChan chan int) {
loop := 1
for loop == 1 {
select {
case msg := <-writeChan:
_, err := sock.Write(msg)
if err != nil {
feedbackChan <- 1
continue
}
case <-feedbackChan:
loop = 0
}
}
}
/*simple write to TCP, for oneway connections only (no communitcation w/ "read" part of the socket
in terms of error propogation)*/
func WriteToTCPw(sock *net.TCPConn, writeChan chan []byte,
feedbackChan chan int) {
feedbackToSocket := make(chan bool)
feedbackFromSocket := make(chan bool)
go writeToTCP(sock, writeChan, feedbackFromSocket, feedbackToSocket)
go func() {
<-feedbackFromSocket
feedbackChan <- 1
}()
}
//simple write to tcp w/ erorr propagation to/from "read" part of the socket
func WriteToTCPrw2(sock *net.TCPConn, writeChan chan []byte,
feedbackChanFromSocket, feedbackChanToSocket chan int) {
loop := 1
for loop == 1 {
select {
case msg := <-writeChan:
_, err := sock.Write(msg)
if err != nil {
select {
case feedbackChanFromSocket <- 1:
continue
case loop = <-feedbackChanToSocket:
loop = 0
continue
}
}
case <-feedbackChanToSocket:
loop = 0
}
}
}
//simple write to tcp w/ erorr propagation to/from "read" part of the socket
func WriteToTCPrw(sock *net.TCPConn, writeChan chan []byte,
feedbackChanFromSocket, feedbackChanToSocket chan int) {
feedbackToSocket := make(chan bool)
feedbackFromSocket := make(chan bool)
go writeToTCP(sock, writeChan, feedbackFromSocket, feedbackToSocket)
go func() {
select {
case <-feedbackFromSocket:
feedbackChanFromSocket <- 1
case <-feedbackChanToSocket:
feedbackToSocket <- true
}
}()
}
func makeBuffer(reuseBufferChan chan []byte) []byte {
select {
case buf := <-reuseBufferChan:
return buf
default:
return make([]byte, 10000)
}
}
//Receive msg from tcp socket and send it as a []byte to readChan,with buffer reuse
func readFromTCP(sock *net.TCPConn, readChan chan []byte,
feedbackFromSocket, feedbackToSocket chan bool,
reuseBufferChan chan []byte) {
loop := 1
var buf []byte
for loop == 1 {
buf = makeBuffer(reuseBufferChan)
bytes, err := sock.Read(buf)
if err != nil {
select {
case feedbackFromSocket <- true:
loop = 0
continue
case <-feedbackToSocket:
loop = 0
continue
}
}
select {
case readChan <- buf[:bytes]:
case <-feedbackToSocket:
loop = 0
continue
}
}
}
//TCP's write routine, with feedback's chans
func writeToTCP(sock *net.TCPConn, writeChan chan []byte,
feedbackFromSocket, feedbackToSocket chan bool) {
loop := 1
for loop == 1 {
select {
case msg := <-writeChan:
_, err := sock.Write(msg)
if err != nil {
select {
case feedbackFromSocket <- true:
case <-feedbackToSocket:
}
loop = 0
continue
}
case <-feedbackToSocket:
loop = 0
continue
}
}
}
//reconnecting to remote host for both read and write purpose
func ReconnectTCPRW(ladr, radr *net.TCPAddr, msgBuf []byte, writeChan chan []byte,
readChan chan []byte, feedbackChanToSocket, feedbackChanFromSocket chan int,
init_msg []byte) {
loop := 1
for loop == 1 {
sock, err := net.DialTCP("tcp", ladr, radr)
if err != nil {
time.Sleep(time.Duration(20+rand.Intn(15)) * time.Second)
continue
}
//testing health of the new socket. GO sometimes doesnt rise the error when
// we receive RST from remote side
_, err = sock.Write(init_msg)
if err != nil {
sock.Close()
time.Sleep(time.Duration(20+rand.Intn(15)) * time.Second)
continue
}
loop = 0
go ReadFromTCP(sock, msgBuf, readChan, feedbackChanFromSocket)
go WriteToTCPrw(sock, writeChan, feedbackChanFromSocket, feedbackChanToSocket)
}
}
func ReconnectTCPRWReuse(ladr, radr *net.TCPAddr,
readChan, writeChan, reuseBufferChan chan []byte,
readFeedbackFrom, readFeedbackTo chan bool,
writeFeedbackFrom, writeFeedbackTo chan bool) {
loop := 1
for loop == 1 {
sock, err := net.DialTCP("tcp", ladr, radr)
if err != nil {
time.Sleep(time.Duration(20+rand.Intn(15)) * time.Second)
continue
}
loop = 0
go readFromTCP(sock, readChan, readFeedbackFrom, readFeedbackTo,
reuseBufferChan)
go writeToTCP(sock, writeChan, writeFeedbackFrom, writeFeedbackTo)
}
}
func AutoRecoonectedTCP(ladr, radr *net.TCPAddr, msgBuf, initMsg []byte,
writeChan, readChan chan []byte, flushChan chan int) {
feedbackChanFromSocket := make(chan int)
feedbackChanToSocket := make(chan int)
go ReconnectTCPRW(ladr, radr, msgBuf, writeChan, readChan, feedbackChanToSocket,
feedbackChanFromSocket, initMsg)
for {
select {
case feedbackFromSocket := <-feedbackChanFromSocket:
feedbackChanToSocket <- feedbackFromSocket
flushChan <- 1
go ReconnectTCPRW(ladr, radr, msgBuf, writeChan,
readChan, feedbackChanToSocket,
feedbackChanFromSocket, initMsg)
}
}
}
func AutoRecoonectedTCPReuse(ladr, radr *net.TCPAddr,
readChan, writeChan chan []byte,
reuseChan chan []byte,
flushChan chan bool) {
readFeedbackFrom := make(chan bool)
readFeedbackTo := make(chan bool)
writeFeedbackFrom := make(chan bool)
writeFeedbackTo := make(chan bool)
go ReconnectTCPRWReuse(ladr, radr, readChan, writeChan, reuseChan,
readFeedbackFrom, readFeedbackTo,
writeFeedbackFrom, writeFeedbackTo)
for {
select {
case <-readFeedbackFrom:
writeFeedbackTo <- true
case <-writeFeedbackFrom:
readFeedbackTo <- true
}
flushChan <- true
go ReconnectTCPRWReuse(ladr, radr, readChan, writeChan, reuseChan,
readFeedbackFrom, readFeedbackTo,
writeFeedbackFrom, writeFeedbackTo)
}
}
//reconnecting to remote host for write only
func ReconnectTCPW(radr net.TCPAddr, writeChan chan []byte, feedbackChan chan int) {
loop := 1
for loop == 1 {
time.Sleep(time.Duration(20+rand.Intn(15)) * time.Second)
sock, err := net.DialTCP("tcp", nil, &radr)
if err != nil {
continue
}
//testing health of the new socket. GO sometimes doesnt rise the error when
// we receive RST from remote side
_, err = sock.Write([]byte{1})
if err != nil {
sock.Close()
continue
}
loop = 0
go WriteToTCPw(sock, writeChan, feedbackChan)
}
}
/* --------------------- CONNECTION MANAGER -------------------------
Connection manager will allow send data and receive data from remote hosts.
it will have single ConnectionMsg (see below) read chan and single
ConnectionMsg write chan toward it's clients
as well as single read chan from sockets, but multiple write sockets.
it will route msgs according to Host field in connectionManager struct
(if it received from client, it will send this msg toward Host's sockets;
if recved from socket, will proxy it toward client(and client will know from which remote host
it was received)
-------------------------------------------------------------------- */
/*
MsgType's could be:
from Api's client to ConnectionManager:
"Data" - msg with Data to Host
"Connect" - connect to new Host
...
from ConnectionManager to Api's client:
"BufferFlush" - notification that connection to remote Host not longer working.
advice to flush all the msg buffers assosiated with remote host
*/
type ConnectionMsg struct {
Host string
Data []byte
Type string
}
/*
Receive msg from tcp socket and send it as a ConnectionMsg to readChan
TODO: think about more generic version to be more DRYer (to work in both CM and []byte chans
*/
func CMReadFromTCP(sock *net.TCPConn, readChan chan ConnectionMsg,
peerAddress string) {
msgBuf := make([]byte, 65000)
loop := 1
var msg ConnectionMsg
msg.Host = peerAddress
msg.Type = "Data"
for loop == 1 {
bytes, err := sock.Read(msgBuf)
if err != nil {
msg.Type = "ReadError"
readChan <- msg
loop = 0
continue
}
b := make([]byte, 0)
b = append(b, msgBuf[:bytes]...)
msg.Data = b
readChan <- msg
}
}
/*
ConnectionManager write instance to tcp w/ erorr propagation to/from "read" part of the socket
TODO: think about more generic version to be more DRYer (to work in both CM and []byte chans
*/
func CMWriteToTCP(sock *net.TCPConn, writeChan, readChan chan ConnectionMsg,
peerAddress string) {
loop := 1
var errorMsg ConnectionMsg
errorMsg.Host = peerAddress
errorMsg.Type = "WriteError"
for loop == 1 {
select {
case msg := <-writeChan:
switch msg.Type {
case "Data":
_, err := sock.Write(msg.Data)
if err != nil {
for loop == 1 {
select {
case readChan <- errorMsg:
case errorMsg := <-writeChan:
if errorMsg.Type != "ConnectionError" {
continue
}
}
loop = 0
}
}
case "ConnectionError":
loop = 0
default:
continue
}
}
}
}
func StartConnection(tcpConn *net.TCPConn, writeChan,
readChan chan ConnectionMsg, peerAddress string) {
go CMReadFromTCP(tcpConn, readChan, peerAddress)
go CMWriteToTCP(tcpConn, writeChan, readChan, peerAddress)
}
func CMListenForConnection(mutex *sync.RWMutex, localPort int,
writeChanMap map[string]chan ConnectionMsg,
connectionStateMap map[string]int,
readChan chan ConnectionMsg) {
laddr := strings.Join([]string{":", strconv.Itoa(localPort)}, "")
tcpLaddr, err := net.ResolveTCPAddr("tcp", laddr)
if err != nil {
panic("cant resolve local address for binding")
}
tcpListener, err := net.ListenTCP("tcp", tcpLaddr)
if err != nil {
panic("cant listen on local address for binding")
}
for {
tcpConn, err := tcpListener.AcceptTCP()
if err == nil {
radr := strings.Split(tcpConn.RemoteAddr().String(), ":")[0]
// check if we already has connection to remote peer as a client
mutex.Lock()
if val, exist := connectionStateMap[radr]; exist && val == 1 {
tcpConn.Close()
mutex.Unlock()
continue
}
connectionStateMap[radr] = 1
if writeChan, exist := writeChanMap[radr]; exist {
mutex.Unlock()
go StartConnection(tcpConn, writeChan, readChan, radr)
} else {
writeChanMap[radr] = make(chan ConnectionMsg)
mutex.Unlock()
go StartConnection(tcpConn, writeChanMap[radr], readChan, radr)
}
}
}
}
func CMConnectToRemotePeer(mutex *sync.RWMutex, peerTcpAddr *net.TCPAddr,
radr string,
writeChan chan ConnectionMsg,
readChan chan ConnectionMsg,
connectionStateMap map[string]int) {
connectLoop := 1
for connectLoop == 1 {
mutex.RLock()
if connectionStateMap[radr] == 1 {
connectLoop = 0
mutex.RUnlock()
continue
}
mutex.RUnlock()
tcpConn, err := net.DialTCP("tcp", nil, peerTcpAddr)
if err != nil {
time.Sleep(time.Second * time.Duration(rand.Int63n(15)))
continue
}
mutex.Lock()
if connectionStateMap[radr] == 0 {
connectionStateMap[radr] = 1
go StartConnection(tcpConn, writeChan, readChan, radr)
mutex.Unlock()
connectLoop = 0
continue
} else {
mutex.Unlock()
tcpConn.Close()
connectLoop = 0
continue
}
}
}
func ConnectionManager(msgChan chan ConnectionMsg, localPort int) {
writeChanMap := make(map[string]chan ConnectionMsg)
connectionStateMap := make(map[string]int)
readChan := make(chan ConnectionMsg)
var connectionMutex sync.RWMutex
go CMListenForConnection(&connectionMutex, localPort, writeChanMap,
connectionStateMap, readChan)
for {
select {
case msgToPeer := <-msgChan:
switch msgToPeer.Type {
case "Data":
if state, exists := connectionStateMap[msgToPeer.Host]; exists && state == 1 {
/*FIXME/THINK: There could be deadlock if connectin closes before we will
be able to send to the chan */
writeChan := writeChanMap[msgToPeer.Host]
writeChan <- msgToPeer
} else {
msgChan <- ConnectionMsg{Type: "ConnectionNotExist"}
}
case "Connect":
if len(strings.Split(msgToPeer.Host, ":")) > 1 {
radr := strings.Split(msgToPeer.Host, ":")[0]
connectionMutex.Lock()
if _, exist := writeChanMap[radr]; !exist {
writeChanMap[radr] = make(chan ConnectionMsg)
}
connectionMutex.Unlock()
peerTcpAddr, err := net.ResolveTCPAddr("tcp", msgToPeer.Host)
if err != nil {
//XXX: think about , mb make something less drastic
panic("cant resolve remote address")
}
go CMConnectToRemotePeer(&connectionMutex, peerTcpAddr, radr,
writeChanMap[radr], readChan, connectionStateMap)
} else {
connectionMutex.Lock()
if _, exist := writeChanMap[msgToPeer.Host]; !exist {
writeChanMap[msgToPeer.Host] = make(chan ConnectionMsg)
}
connectionMutex.Unlock()
remoteAddr := strings.Join([]string{msgToPeer.Host, strconv.Itoa(localPort)}, ":")
peerTcpAddr, err := net.ResolveTCPAddr("tcp", remoteAddr)
if err != nil {
//XXX: again panic could be overkill
panic("cant resolve remote address")
}
go CMConnectToRemotePeer(&connectionMutex, peerTcpAddr, msgToPeer.Host,
writeChanMap[msgToPeer.Host], readChan, connectionStateMap)
}
}
case msgFromPeer := <-readChan:
switch msgFromPeer.Type {
case "Data":
msgChan <- msgFromPeer
case "WriteError", "ReadError":
connectionMutex.Lock()
connectionStateMap[msgFromPeer.Host] = 0
connectionMutex.Unlock()
if msgFromPeer.Type == "ReadError" {
writeChanMap[msgFromPeer.Host] <- ConnectionMsg{Type: "ConnectionError"}
}
var msgToApiClient ConnectionMsg
msgToApiClient.Host = msgFromPeer.Host
msgToApiClient.Type = "BufferFlush"
msgChan <- msgToApiClient
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
package netutils
import (
"errors"
"net"
)
/*
we need to provide a function, which will read/write to/from socket and read/write to/from sockets feedback chans
*/
func ListenForConnection(port string, fn func(chan []byte, chan []byte, chan int, chan int)) error {
addr := ":" + port
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return errors.New("cant resolve local tcp address")
}
loop := 1
servSock, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
return errors.New("cant bind to local tcp address")
}
for loop == 1 {
sock, err := servSock.AcceptTCP()
if err != nil {
continue
}
go ServeTcpConn(sock, fn)
}
return nil
}
func ServeTcpConn(sock *net.TCPConn, fn func(chan []byte, chan []byte, chan int, chan int)) {
readChan := make(chan []byte)
writeChan := make(chan []byte)
feedbackFrom := make(chan int, 1)
feedbackTo := make(chan int, 1)
buf := make([]byte, 65535)
go ReadFromTCP(sock, buf, readChan, feedbackFrom)
go WriteToTCPrw(sock, writeChan, feedbackFrom, feedbackTo)
fn(readChan, writeChan, feedbackFrom, feedbackTo)
}
+3
View File
@@ -1,3 +1,6 @@
# github.com/leprosus/golang-ttl-map v1.1.7 # github.com/leprosus/golang-ttl-map v1.1.7
## explicit; go 1.15 ## explicit; go 1.15
github.com/leprosus/golang-ttl-map github.com/leprosus/golang-ttl-map
# github.com/tehnerd/goUtils v0.0.0-20150515130609-5a2d8fb2ded8
## explicit
github.com/tehnerd/goUtils/netutils