diff --git a/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go b/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go index d6be404c2..684e5502e 100644 --- a/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go +++ b/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go @@ -1,6 +1,7 @@ package accesslogs import ( + "maps" "net" "net/netip" "time" @@ -57,7 +58,7 @@ func (a *AccessLogEntry) FromProto(serviceLog *proto.AccessLog) { a.BytesDownload = serviceLog.GetBytesDownload() a.Protocol = AccessLogProtocol(serviceLog.GetProtocol()) if m := serviceLog.GetMetadata(); len(m) > 0 { - a.Metadata = m + a.Metadata = maps.Clone(m) } if sourceIP := serviceLog.GetSourceIp(); sourceIP != "" { diff --git a/management/internals/modules/reverseproxy/proxy/manager/manager.go b/management/internals/modules/reverseproxy/proxy/manager/manager.go index 08d1e10bb..d13334e83 100644 --- a/management/internals/modules/reverseproxy/proxy/manager/manager.go +++ b/management/internals/modules/reverseproxy/proxy/manager/manager.go @@ -139,8 +139,8 @@ func (m Manager) ClusterRequireSubdomain(ctx context.Context, clusterAddr string return m.store.GetClusterRequireSubdomain(ctx, clusterAddr) } -// ClusterSupportsCrowdSec returns whether any active proxy in the cluster -// has CrowdSec configured. Returns nil when no proxy has reported capabilities. +// ClusterSupportsCrowdSec returns whether all active proxies in the cluster +// have CrowdSec configured (unanimous). Returns nil when no proxy has reported capabilities. func (m Manager) ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool { return m.store.GetClusterSupportsCrowdSec(ctx, clusterAddr) } diff --git a/proxy/internal/auth/middleware.go b/proxy/internal/auth/middleware.go index 7dfdda883..f1d1fcc59 100644 --- a/proxy/internal/auth/middleware.go +++ b/proxy/internal/auth/middleware.go @@ -170,6 +170,9 @@ func (mw *Middleware) checkIPRestrictions(w http.ResponseWriter, r *http.Request if verdict.IsCrowdSec() { if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil { cd.SetMetadata("crowdsec_verdict", verdict.String()) + if config.IPRestrictions.IsObserveOnly(verdict) { + cd.SetMetadata("crowdsec_mode", "observe") + } } } diff --git a/proxy/internal/crowdsec/bouncer.go b/proxy/internal/crowdsec/bouncer.go index edd76cb25..06a452520 100644 --- a/proxy/internal/crowdsec/bouncer.go +++ b/proxy/internal/crowdsec/bouncer.go @@ -4,6 +4,7 @@ package crowdsec import ( "context" + "errors" "net/netip" "strings" "sync" @@ -83,6 +84,11 @@ func (b *Bouncer) Start(ctx context.Context) error { done := make(chan struct{}) b.lifeMu.Lock() + if b.cancel != nil { + b.lifeMu.Unlock() + cancel() + return errors.New("bouncer already started") + } b.cancel = cancel b.done = done b.lifeMu.Unlock() diff --git a/proxy/internal/restrict/restrict.go b/proxy/internal/restrict/restrict.go index 7e9f90a58..f3e0fa695 100644 --- a/proxy/internal/restrict/restrict.go +++ b/proxy/internal/restrict/restrict.go @@ -174,7 +174,12 @@ func (v Verdict) String() string { // IsCrowdSec returns true when the verdict originates from a CrowdSec check. func (v Verdict) IsCrowdSec() bool { - return v >= DenyCrowdSecBan && v <= DenyCrowdSecUnavailable + switch v { + case DenyCrowdSecBan, DenyCrowdSecCaptcha, DenyCrowdSecThrottle, DenyCrowdSecUnavailable: + return true + default: + return false + } } // IsObserveOnly returns true when v is a CrowdSec verdict and the filter is in @@ -306,5 +311,5 @@ func (f *Filter) HasRestrictions() bool { } return len(f.AllowedCIDRs) > 0 || len(f.BlockedCIDRs) > 0 || len(f.AllowedCountries) > 0 || len(f.BlockedCountries) > 0 || - (f.CrowdSec != nil && f.CrowdSecMode != CrowdSecOff) + f.CrowdSecMode == CrowdSecEnforce || f.CrowdSecMode == CrowdSecObserve } diff --git a/proxy/internal/restrict/restrict_test.go b/proxy/internal/restrict/restrict_test.go index ebb85e888..398025cee 100644 --- a/proxy/internal/restrict/restrict_test.go +++ b/proxy/internal/restrict/restrict_test.go @@ -403,4 +403,9 @@ func TestFilter_HasRestrictions_CrowdSec(t *testing.T) { cs := &mockCrowdSec{ready: true} f := ParseFilter(FilterConfig{CrowdSec: cs, CrowdSecMode: CrowdSecEnforce}) assert.True(t, f.HasRestrictions()) + + // Enforce mode without checker (LAPI not configured): still has restrictions + // because Check() will fail-closed with DenyCrowdSecUnavailable. + f2 := ParseFilter(FilterConfig{CrowdSec: nil, CrowdSecMode: CrowdSecEnforce}) + assert.True(t, f2.HasRestrictions()) }