mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
✨ add testing for redis with mock
This commit is contained in:
@@ -4,7 +4,7 @@ export GO111MODULE=on
|
|||||||
|
|
||||||
# Binary/mock suite (Traefik binary + mock LAPI). This is what CI runs.
|
# Binary/mock suite (Traefik binary + mock LAPI). This is what CI runs.
|
||||||
# The local Docker suite (make e2e) lives in a separate PR/branch.
|
# The local Docker suite (make e2e) lives in a separate PR/branch.
|
||||||
E2E_MOCK_SCENARIOS := stream-mode live-mode none-mode trusted-ips custom-ban-page captcha appsec tls-system-ca
|
E2E_MOCK_SCENARIOS := $(notdir $(wildcard tests/e2e/mock/scenarios/*))
|
||||||
|
|
||||||
default: lint test
|
default: lint test
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ WEB_PORT="${WEB_PORT:-8000}"
|
|||||||
LAPI_PORT="${LAPI_PORT:-8090}"
|
LAPI_PORT="${LAPI_PORT:-8090}"
|
||||||
BACKEND_PORT="${BACKEND_PORT:-8091}"
|
BACKEND_PORT="${BACKEND_PORT:-8091}"
|
||||||
APPSEC_PORT="${APPSEC_PORT:-8092}"
|
APPSEC_PORT="${APPSEC_PORT:-8092}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-8093}"
|
||||||
LAPI_KEY="${LAPI_KEY:-e2e-mock-key}"
|
LAPI_KEY="${LAPI_KEY:-e2e-mock-key}"
|
||||||
|
|
||||||
MOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
MOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -187,6 +188,7 @@ start_stack() {
|
|||||||
-e "s|@@LAPI_HOST@@|127.0.0.1:${LAPI_PORT}|g" \
|
-e "s|@@LAPI_HOST@@|127.0.0.1:${LAPI_PORT}|g" \
|
||||||
-e "s|@@APPSEC_HOST@@|127.0.0.1:${APPSEC_PORT}|g" \
|
-e "s|@@APPSEC_HOST@@|127.0.0.1:${APPSEC_PORT}|g" \
|
||||||
-e "s|@@BACKEND_URL@@|http://127.0.0.1:${BACKEND_PORT}|g" \
|
-e "s|@@BACKEND_URL@@|http://127.0.0.1:${BACKEND_PORT}|g" \
|
||||||
|
-e "s|@@REDIS_HOST@@|127.0.0.1:${REDIS_PORT}|g" \
|
||||||
-e "s|@@SCENARIO_DIR@@|${scenario_dir}|g" \
|
-e "s|@@SCENARIO_DIR@@|${scenario_dir}|g" \
|
||||||
"$scenario_dir/dynamic.yml" > "$WORKDIR/dynamic.yml"
|
"$scenario_dir/dynamic.yml" > "$WORKDIR/dynamic.yml"
|
||||||
|
|
||||||
@@ -203,6 +205,7 @@ start_stack() {
|
|||||||
--lapi-addr "127.0.0.1:${LAPI_PORT}" \
|
--lapi-addr "127.0.0.1:${LAPI_PORT}" \
|
||||||
--backend-addr "127.0.0.1:${BACKEND_PORT}" \
|
--backend-addr "127.0.0.1:${BACKEND_PORT}" \
|
||||||
--appsec-addr "127.0.0.1:${APPSEC_PORT}" \
|
--appsec-addr "127.0.0.1:${APPSEC_PORT}" \
|
||||||
|
--redis-addr "127.0.0.1:${REDIS_PORT}" \
|
||||||
"${mock_tls_args[@]}" >"$WORKDIR/mock.log" 2>&1 &
|
"${mock_tls_args[@]}" >"$WORKDIR/mock.log" 2>&1 &
|
||||||
MOCK_PID=$!
|
MOCK_PID=$!
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
// suite. It answers only the few LAPI routes the plugin calls — live/none
|
// suite. It answers only the few LAPI routes the plugin calls — live/none
|
||||||
// decision lookups, the stream poll and the usage-metrics push — and lets the
|
// decision lookups, the stream poll and the usage-metrics push — and lets the
|
||||||
// test drive decisions through /admin instead of `cscli`. It also serves the
|
// test drive decisions through /admin instead of `cscli`. It also serves the
|
||||||
// stub upstream that Traefik proxies allowed requests to.
|
// stub upstream that Traefik proxies allowed requests to, and a hardcoded Redis
|
||||||
|
// stand-in for exercising the redis cache path.
|
||||||
//
|
//
|
||||||
// It is NOT a Crowdsec/AppSec conformance harness — the real WAF engine (OWASP
|
// It is NOT a Crowdsec/AppSec conformance harness — the real WAF engine (OWASP
|
||||||
// CRS, virtual patching) is out of scope. The AppSec endpoint here emulates a
|
// CRS, virtual patching) is out of scope. The AppSec endpoint here emulates a
|
||||||
@@ -11,10 +12,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -46,6 +49,48 @@ func list(m map[string]Decision) []Decision {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Redis mock (inline-command wire format, as spoken by simpleredis) ---
|
||||||
|
|
||||||
|
// serveRedis is a hardcoded stand-in. Every line is scanned for known IPs:
|
||||||
|
// 1.2.3.4 → "t", 1.2.3.5 → "f", GET for anything else → miss ($-1).
|
||||||
|
// SET, DEL, AUTH, SELECT get +OK (they don't read the response anyway).
|
||||||
|
func serveRedis(addr string) {
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
log.Printf("mocklapi: Redis mock listening on %s", addr)
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
rd := bufio.NewReader(conn)
|
||||||
|
for {
|
||||||
|
line, _, err := rd.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := string(line)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(s, "1.2.3.4"):
|
||||||
|
conn.Write([]byte("$1\r\nf\r\n"))
|
||||||
|
case strings.Contains(s, "1.2.3.5"):
|
||||||
|
conn.Write([]byte("$1\r\nt\r\n"))
|
||||||
|
case strings.HasPrefix(strings.ToUpper(s), "GET "):
|
||||||
|
conn.Write([]byte("$-1\r\n"))
|
||||||
|
default:
|
||||||
|
conn.Write([]byte("+OK\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
lapiAddr := flag.String("lapi-addr", "127.0.0.1:8090", "address for the LAPI mock")
|
lapiAddr := flag.String("lapi-addr", "127.0.0.1:8090", "address for the LAPI mock")
|
||||||
// The stub upstream Traefik proxies allowed requests to — the binary-suite
|
// The stub upstream Traefik proxies allowed requests to — the binary-suite
|
||||||
@@ -53,6 +98,9 @@ func main() {
|
|||||||
backendAddr := flag.String("backend-addr", "127.0.0.1:8091", "address for the stub upstream service")
|
backendAddr := flag.String("backend-addr", "127.0.0.1:8091", "address for the stub upstream service")
|
||||||
// AppSec WAF stand-in (the real engine listens on :7422). Not a CRS engine.
|
// AppSec WAF stand-in (the real engine listens on :7422). Not a CRS engine.
|
||||||
appsecAddr := flag.String("appsec-addr", "127.0.0.1:8092", "address for the AppSec mock")
|
appsecAddr := flag.String("appsec-addr", "127.0.0.1:8092", "address for the AppSec mock")
|
||||||
|
// Redis stand-in: the mock always serves a hardcoded GET on a plain TCP
|
||||||
|
// port, enough to exercise the plugin's redis cache path.
|
||||||
|
redisAddr := flag.String("redis-addr", "127.0.0.1:8093", "address for the Redis mock")
|
||||||
// Optional TLS for the LAPI: when both are set the LAPI is served over HTTPS
|
// Optional TLS for the LAPI: when both are set the LAPI is served over HTTPS
|
||||||
// (cert signed by the scenario's throwaway CA) so the suite can exercise the
|
// (cert signed by the scenario's throwaway CA) so the suite can exercise the
|
||||||
// bouncer's system-trust-store path. Backend and AppSec stay plaintext.
|
// bouncer's system-trust-store path. Backend and AppSec stay plaintext.
|
||||||
@@ -96,6 +144,8 @@ func main() {
|
|||||||
})))
|
})))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go serveRedis(*redisAddr)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// Readiness probe for the test harness (empty body, 200).
|
// Readiness probe for the test harness (empty body, 200).
|
||||||
@@ -154,9 +204,9 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if *lapiTLSCert != "" && *lapiTLSKey != "" {
|
if *lapiTLSCert != "" && *lapiTLSKey != "" {
|
||||||
log.Printf("mocklapi: LAPI on %s (TLS), backend on %s, appsec on %s", *lapiAddr, *backendAddr, *appsecAddr)
|
log.Printf("mocklapi: LAPI on %s (TLS), backend on %s, appsec on %s, redis on %s", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr)
|
||||||
log.Fatal(http.ListenAndServeTLS(*lapiAddr, *lapiTLSCert, *lapiTLSKey, mux))
|
log.Fatal(http.ListenAndServeTLS(*lapiAddr, *lapiTLSCert, *lapiTLSKey, mux))
|
||||||
}
|
}
|
||||||
log.Printf("mocklapi: LAPI on %s, backend on %s, appsec on %s", *lapiAddr, *backendAddr, *appsecAddr)
|
log.Printf("mocklapi: LAPI on %s, backend on %s, appsec on %s, redis on %s", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr)
|
||||||
log.Fatal(http.ListenAndServe(*lapiAddr, mux))
|
log.Fatal(http.ListenAndServe(*lapiAddr, mux))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
r:
|
||||||
|
rule: "PathPrefix(`/foo`)"
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: backend
|
||||||
|
middlewares:
|
||||||
|
- bouncer
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "@@BACKEND_URL@@"
|
||||||
|
middlewares:
|
||||||
|
bouncer:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
enabled: "true"
|
||||||
|
crowdsecMode: live
|
||||||
|
crowdsecLapiScheme: http
|
||||||
|
crowdsecLapiHost: "@@LAPI_HOST@@"
|
||||||
|
crowdsecLapiKey: "@@APIKEY@@"
|
||||||
|
redisCacheEnabled: "true"
|
||||||
|
redisCacheHost: "@@REDIS_HOST@@"
|
||||||
|
forwardedHeadersTrustedIps:
|
||||||
|
- "127.0.0.1/32"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=../../lib/common.sh
|
||||||
|
source "$HERE/../../lib/common.sh"
|
||||||
|
|
||||||
|
SCENARIO=redis
|
||||||
|
|
||||||
|
# Redis cache check: the mock returns "t" (banned) for 1.2.3.4 and "f" (not
|
||||||
|
# banned) for 1.2.3.5. All other IPs return a miss, which falls through to the
|
||||||
|
# LAPI (no decision → allowed). This proves the plugin reads cached decisions
|
||||||
|
# from Redis correctly.
|
||||||
|
body() {
|
||||||
|
echo "[$SCENARIO] cached clean IP must pass"
|
||||||
|
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 200 -H "X-Forwarded-For: 1.2.3.4"
|
||||||
|
|
||||||
|
echo "[$SCENARIO] cached banned IP must be blocked"
|
||||||
|
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 403 -H "X-Forwarded-For: 1.2.3.5"
|
||||||
|
|
||||||
|
echo "[$SCENARIO] unknown IP (redis miss) must fall through to LAPI and pass"
|
||||||
|
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 200 -H "X-Forwarded-For: 1.2.3.6"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_scenario "$SCENARIO" "$HERE" body
|
||||||
Reference in New Issue
Block a user