From 65d5a30e7caddeeca1f4068cd2d3ee8904c8046c Mon Sep 17 00:00:00 2001 From: Russoul Date: Mon, 16 Mar 2026 14:50:50 +0300 Subject: [PATCH] bench: Introduce DDoS protection middleware (wai) to cardano-tracer --- cardano-tracer/cardano-tracer.cabal | 3 + .../Metrics/DDoSProtectionMiddleware.hs | 59 +++++++++++++++++++ .../Tracer/Handlers/Metrics/Monitoring.hs | 19 ++++-- .../Tracer/Handlers/Metrics/Prometheus.hs | 18 +++++- .../Handlers/Metrics/TimeseriesServer.hs | 12 +++- 5 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs diff --git a/cardano-tracer/cardano-tracer.cabal b/cardano-tracer/cardano-tracer.cabal index 2f13edf7041..9a2f6bc1acf 100644 --- a/cardano-tracer/cardano-tracer.cabal +++ b/cardano-tracer/cardano-tracer.cabal @@ -120,6 +120,7 @@ library Cardano.Tracer.Handlers.Logs.TraceObjects Cardano.Tracer.Handlers.Logs.Utils + Cardano.Tracer.Handlers.Metrics.DDoSProtectionMiddleware Cardano.Tracer.Handlers.Metrics.Monitoring Cardano.Tracer.Handlers.Metrics.Prometheus Cardano.Tracer.Handlers.Metrics.Servers @@ -202,6 +203,8 @@ library , trace-forward ^>= 2.4.0 , trace-resources ^>= 0.2.4 , wai ^>= 3.2 + , wai-extra + , wai-rate-limit , warp ^>= 3.4 , warp-tls , yaml diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs new file mode 100644 index 00000000000..3f38f64eb10 --- /dev/null +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedRecordDot #-} +module Cardano.Tracer.Handlers.Metrics.DDoSProtectionMiddleware(DDoSProtectionMiddlewareConfig(..), mkDDoSProtectionMiddleware) where +import Control.Concurrent (forkIO) +import Control.Concurrent.Extra (threadDelay) +import Control.Concurrent.STM (atomically, modifyTVar', newTVarIO, readTVar, readTVarIO, + writeTVar) +import Control.Monad (void) +import Network.Wai +import Network.Wai.Middleware.RequestSizeLimit +import Network.Wai.Middleware.Timeout +import Network.Wai.RateLimit +import Network.Wai.RateLimit.Backend (Backend (MkBackend)) +import Network.Wai.RateLimit.Strategy + +data DDoSProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { + requestBodySizeLimitKB :: Word, + requestRateWindowSec :: Word, + requestRateLimitSec :: Word, + responseTimeLimitSec :: Word +} + +-- | Simple request rate limiter backend that limits the rate of +-- requests based on the total number of requests. +totalRequestRateLimiterBackend :: IO (Backend ()) +totalRequestRateLimiterBackend = do + usage <- newTVarIO (0 :: Integer) + + let + backendGetUsage :: () -> IO Integer + backendGetUsage _ = readTVarIO usage + + backendIncAndGetUsage :: () -> Integer -> IO Integer + backendIncAndGetUsage _ k = atomically $ modifyTVar' usage (+ k) >> readTVar usage + + backendExpireIn :: () -> Integer -> IO () + backendExpireIn _ s = void $ forkIO $ do + threadDelay (fromIntegral (s * 1_000_000)) + atomically $ writeTVar usage 0 + + pure $ MkBackend backendGetUsage backendIncAndGetUsage backendExpireIn + +mkDDoSProtectionMiddleware :: DDoSProtectionMiddlewareConfig -> IO Middleware +mkDDoSProtectionMiddleware cfg = totalRequestRateLimiterBackend >>= \backend -> + pure $ + -- request body size limiter + requestSizeLimitMiddleware + (setMaxLengthForRequest (const (pure (Just (fromIntegral cfg.requestBodySizeLimitKB * 1024)))) + defaultRequestSizeLimitSettings) + . + -- request rate limiter (fixed window) + rateLimiting (fixedWindow backend + (fromIntegral cfg.requestRateWindowSec) + (fromIntegral cfg.requestRateLimitSec) + (const (pure ())) + ) + . + -- response time limiter + timeout (fromIntegral cfg.responseTimeLimitSec) diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs index ca0e4ed8dde..c61de7fbda7 100644 --- a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs @@ -1,6 +1,6 @@ {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} module Cardano.Tracer.Handlers.Metrics.Monitoring @@ -9,6 +9,7 @@ module Cardano.Tracer.Handlers.Metrics.Monitoring import Cardano.Tracer.Configuration import Cardano.Tracer.Environment +import Cardano.Tracer.Handlers.Metrics.DDoSProtectionMiddleware import Cardano.Tracer.Handlers.Metrics.Utils import Cardano.Tracer.MetaTrace import Cardano.Tracer.Types @@ -22,11 +23,19 @@ import qualified Data.Text as T import Network.HTTP.Types import Network.Wai import Network.Wai.Handler.Warp (Settings, defaultSettings, runSettings) -import Network.Wai.Handler.WarpTLS (runTLS, tlsSettingsChain, TLSSettings) +import Network.Wai.Handler.WarpTLS (TLSSettings, runTLS, tlsSettingsChain) import qualified System.Metrics as EKG import System.Remote.Monitoring.Wai import System.Time.Extra (sleep) +ddosProtectionMiddlewareConfig :: DDoSProtectionMiddlewareConfig +ddosProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { + requestBodySizeLimitKB = 2 * 1024, + requestRateWindowSec = 60, + requestRateLimitSec = 120, + responseTimeLimitSec = 5 +} + -- | 'ekg' package allows to run only one EKG server, to display only one web page -- for particular EKG.Store. Since 'cardano-tracer' can be connected to any number -- of nodes, we display their list on the first web page (the first 'Endpoint') @@ -43,7 +52,7 @@ runMonitoringServer -> IO RouteDictionary -> IO () runMonitoringServer tracerEnv endpoint computeRoutes_autoUpdate = do - let TracerEnv + let TracerEnv { teConfig = TracerConfig { tlsCertificate } , teTracer @@ -57,6 +66,8 @@ runMonitoringServer tracerEnv endpoint computeRoutes_autoUpdate = do } dummyStore <- EKG.newStore + middleware <- mkDDoSProtectionMiddleware ddosProtectionMiddlewareConfig + let settings :: Settings settings = setEndpoint endpoint defaultSettings @@ -66,7 +77,7 @@ runMonitoringServer tracerEnv endpoint computeRoutes_autoUpdate = do tlsSettingsChain certificateFile (fromMaybe [] certificateChain) certificateKeyFile application :: Application - application = renderEkg dummyStore computeRoutes_autoUpdate + application = middleware $ renderEkg dummyStore computeRoutes_autoUpdate run :: IO () run | Just True <- epForceSSL endpoint diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs index 6338404ddaa..060687244a1 100644 --- a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs @@ -1,6 +1,6 @@ {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ViewPatterns #-} @@ -11,6 +11,7 @@ module Cardano.Tracer.Handlers.Metrics.Prometheus import Cardano.Logging.Prometheus.Exposition (renderExpositionFromSampleWith) import Cardano.Tracer.Configuration import Cardano.Tracer.Environment +import Cardano.Tracer.Handlers.Metrics.DDoSProtectionMiddleware import Cardano.Tracer.Handlers.Metrics.Utils import Cardano.Tracer.MetaTrace @@ -30,10 +31,18 @@ import qualified Data.Text.Lazy.Encoding as TL import Network.HTTP.Types import Network.Wai import Network.Wai.Handler.Warp (Settings, defaultSettings, runSettings) -import Network.Wai.Handler.WarpTLS (runTLS, tlsSettingsChain, TLSSettings) +import Network.Wai.Handler.WarpTLS (TLSSettings, runTLS, tlsSettingsChain) import System.Metrics as EKG (Store, sampleAll) import System.Time.Extra (sleep) +ddosProtectionMiddlewareConfig :: DDoSProtectionMiddlewareConfig +ddosProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { + requestBodySizeLimitKB = 2 * 1024, + requestRateWindowSec = 60, + requestRateLimitSec = 120, + responseTimeLimitSec = 5 +} + -- | Runs a simple HTTP server that listens on @endpoint@. -- -- At the root, it lists the connected nodes, either as HTML or JSON, depending @@ -103,6 +112,9 @@ runPrometheusServer tracerEnv endpoint computeRoutes_autoUpdate = do traceWith teTracer TracerStartedPrometheus { ttPrometheusEndpoint = endpoint } + + middleware <- mkDDoSProtectionMiddleware ddosProtectionMiddlewareConfig + let settings :: Settings settings = setEndpoint endpoint defaultSettings @@ -112,7 +124,7 @@ runPrometheusServer tracerEnv endpoint computeRoutes_autoUpdate = do tlsSettingsChain certificateFile (fromMaybe [] certificateChain) certificateKeyFile application :: Application - application = renderPrometheus computeRoutes_autoUpdate noSuffix teMetricsHelp promLabels + application = middleware $ renderPrometheus computeRoutes_autoUpdate noSuffix teMetricsHelp promLabels run :: IO () run | Just True <- epForceSSL endpoint diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/TimeseriesServer.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/TimeseriesServer.hs index 258619cc96a..79d102553d6 100644 --- a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/TimeseriesServer.hs +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/TimeseriesServer.hs @@ -10,6 +10,7 @@ import Cardano.Timeseries.Interface (ExecutionError (..)) import Cardano.Tracer.Acceptors.Utils (getTimeMs) import Cardano.Tracer.Configuration (Certificate (..), Endpoint, TracerConfig (..), epForceSSL, setEndpoint) +import Cardano.Tracer.Handlers.Metrics.DDoSProtectionMiddleware import Cardano.Tracer.Handlers.Metrics.Utils (contentHdrUtf8Text) import Cardano.Tracer.MetaTrace import Cardano.Tracer.Timeseries @@ -25,6 +26,14 @@ import Network.Wai.Handler.Warp hiding (run) import Network.Wai.Handler.WarpTLS import System.Time.Extra (sleep) +ddosProtectionMiddlewareConfig :: DDoSProtectionMiddlewareConfig +ddosProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { + requestBodySizeLimitKB = 2 * 1024, + requestRateWindowSec = 60, + requestRateLimitSec = 30, + responseTimeLimitSec = 5 +} + -- | GET timeseries/query parseTimeseriesQuery :: Request -> Maybe () parseTimeseriesQuery request = do @@ -57,6 +66,7 @@ runTimeseriesServer tr tracerConfig endpoint handle = do { ttTimeseriesEndpoint = endpoint } + middleware <- mkDDoSProtectionMiddleware ddosProtectionMiddlewareConfig let settings :: Settings @@ -67,7 +77,7 @@ runTimeseriesServer tr tracerConfig endpoint handle = do tlsSettingsChain certificateFile (fromMaybe [] certificateChain) certificateKeyFile application :: Application - application = timeseriesApp handle + application = middleware $ timeseriesApp handle run :: IO () run | Just True <- epForceSSL endpoint , Just cert <- tlsCertificate tracerConfig