From 65d5a30e7caddeeca1f4068cd2d3ee8904c8046c Mon Sep 17 00:00:00 2001 From: Russoul Date: Mon, 16 Mar 2026 14:50:50 +0300 Subject: [PATCH 1/4] 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 From a9c47a7a76f371cdef64a5d6b2ab9d8e61806cfb Mon Sep 17 00:00:00 2001 From: Russoul Date: Thu, 19 Mar 2026 19:34:25 +0300 Subject: [PATCH 2/4] Fix record styling --- .../Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs index 3f38f64eb10..7040798caaf 100644 --- a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs @@ -14,10 +14,10 @@ import Network.Wai.RateLimit.Backend (Backend (MkBackend)) import Network.Wai.RateLimit.Strategy data DDoSProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { - requestBodySizeLimitKB :: Word, - requestRateWindowSec :: Word, - requestRateLimitSec :: Word, - responseTimeLimitSec :: Word + requestBodySizeLimitKB :: Word + , requestRateWindowSec :: Word + , requestRateLimitSec :: Word + , responseTimeLimitSec :: Word } -- | Simple request rate limiter backend that limits the rate of From 2a90af44fc0a47d800d12c4190f27b13a2fe5b03 Mon Sep 17 00:00:00 2001 From: Russoul Date: Thu, 19 Mar 2026 19:36:44 +0300 Subject: [PATCH 3/4] Tune middleware configs of Monitoring & Prometheus servers --- .../src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs | 2 +- .../src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs index c61de7fbda7..95ef385b7b7 100644 --- a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Monitoring.hs @@ -32,7 +32,7 @@ ddosProtectionMiddlewareConfig :: DDoSProtectionMiddlewareConfig ddosProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { requestBodySizeLimitKB = 2 * 1024, requestRateWindowSec = 60, - requestRateLimitSec = 120, + requestRateLimitSec = 600, responseTimeLimitSec = 5 } diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs index 060687244a1..12a4ffe27aa 100644 --- a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Prometheus.hs @@ -37,10 +37,10 @@ import System.Time.Extra (sleep) ddosProtectionMiddlewareConfig :: DDoSProtectionMiddlewareConfig ddosProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { - requestBodySizeLimitKB = 2 * 1024, + requestBodySizeLimitKB = 4, requestRateWindowSec = 60, requestRateLimitSec = 120, - responseTimeLimitSec = 5 + responseTimeLimitSec = 2 } -- | Runs a simple HTTP server that listens on @endpoint@. From fc5f963cd576e0a61a2c9054dfa891fdc954d21a Mon Sep 17 00:00:00 2001 From: Russoul Date: Thu, 19 Mar 2026 19:55:57 +0300 Subject: [PATCH 4/4] `stateTVar` & `diag` --- .../Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs index 7040798caaf..22589240351 100644 --- a/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs +++ b/cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/DDoSProtectionMiddleware.hs @@ -3,8 +3,7 @@ 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.Concurrent.STM (atomically, newTVarIO, readTVarIO, stateTVar, writeTVar) import Control.Monad (void) import Network.Wai import Network.Wai.Middleware.RequestSizeLimit @@ -20,6 +19,10 @@ data DDoSProtectionMiddlewareConfig = DDoSProtectionMiddlewareConfig { , responseTimeLimitSec :: Word } +-- COMMENT: (@russoul) do we have a good place for this function (named after the diagonal functor)? +diag :: a -> (a, a) +diag x = (x, x) + -- | Simple request rate limiter backend that limits the rate of -- requests based on the total number of requests. totalRequestRateLimiterBackend :: IO (Backend ()) @@ -31,7 +34,7 @@ totalRequestRateLimiterBackend = do backendGetUsage _ = readTVarIO usage backendIncAndGetUsage :: () -> Integer -> IO Integer - backendIncAndGetUsage _ k = atomically $ modifyTVar' usage (+ k) >> readTVar usage + backendIncAndGetUsage _ k = atomically $ stateTVar usage (diag . (+ k)) backendExpireIn :: () -> Integer -> IO () backendExpireIn _ s = void $ forkIO $ do