@@ -79,6 +79,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
7979 /// This object deploys heuristics to attempt to detect denial of service attacks.
8080 private var denialOfServiceValidator : DOSHeuristics < RealNIODeadlineClock >
8181
82+ private var glitchesMonitor : GlitchesMonitor
83+
8284 /// The mode this handler is operating in.
8385 private let mode : ParserMode
8486
@@ -236,6 +238,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
236238 maximumBufferedControlFrames: 10000 ,
237239 maximumSequentialContinuationFrames: NIOHTTP2Handler . defaultMaximumSequentialContinuationFrames,
238240 maximumRecentlyResetStreams: Self . defaultMaximumRecentlyResetFrames,
241+ maximumConnectionGlitches: GlitchesMonitor . defaultMaximumGlitches,
239242 maximumResetFrameCount: 200 ,
240243 resetFrameCounterWindow: . seconds( 30 ) ,
241244 maximumStreamErrorCount: 200 ,
@@ -273,6 +276,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
273276 maximumBufferedControlFrames: maximumBufferedControlFrames,
274277 maximumSequentialContinuationFrames: NIOHTTP2Handler . defaultMaximumSequentialContinuationFrames,
275278 maximumRecentlyResetStreams: Self . defaultMaximumRecentlyResetFrames,
279+ maximumConnectionGlitches: GlitchesMonitor . defaultMaximumGlitches,
276280 maximumResetFrameCount: 200 ,
277281 resetFrameCounterWindow: . seconds( 30 ) ,
278282 maximumStreamErrorCount: 200 ,
@@ -302,6 +306,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
302306 maximumBufferedControlFrames: connectionConfiguration. maximumBufferedControlFrames,
303307 maximumSequentialContinuationFrames: connectionConfiguration. maximumSequentialContinuationFrames,
304308 maximumRecentlyResetStreams: connectionConfiguration. maximumRecentlyResetStreams,
309+ maximumConnectionGlitches: connectionConfiguration. maximumConnectionGlitches,
305310 maximumResetFrameCount: streamConfiguration. streamResetFrameRateLimit. maximumCount,
306311 resetFrameCounterWindow: streamConfiguration. streamResetFrameRateLimit. windowLength,
307312 maximumStreamErrorCount: streamConfiguration. streamErrorRateLimit. maximumCount,
@@ -319,6 +324,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
319324 maximumBufferedControlFrames: Int ,
320325 maximumSequentialContinuationFrames: Int ,
321326 maximumRecentlyResetStreams: Int ,
327+ maximumConnectionGlitches: Int ,
322328 maximumResetFrameCount: Int ,
323329 resetFrameCounterWindow: TimeAmount ,
324330 maximumStreamErrorCount: Int ,
@@ -348,6 +354,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
348354 self . tolerateImpossibleStateTransitionsInDebugMode = false
349355 self . inboundStreamMultiplexerState = . uninitializedLegacy
350356 self . maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
357+ self . glitchesMonitor = GlitchesMonitor ( maximumGlitches: maximumConnectionGlitches)
351358 }
352359
353360 /// Constructs a ``NIOHTTP2Handler``.
@@ -369,6 +376,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
369376 /// against this DoS vector we put an upper limit on this rate. Defaults to 200.
370377 /// - resetFrameCounterWindow: Controls the sliding window used to enforce the maximum permitted reset frames rate. Too many may exhaust CPU resources. To protect
371378 /// against this DoS vector we put an upper limit on this rate. 30 seconds.
379+ /// - maximumConnectionGlitches: Controls the maximum number of stream errors that can happen on a connection before the connection is reset. Defaults to 200.
372380 internal init (
373381 mode: ParserMode ,
374382 initialSettings: HTTP2Settings = nioDefaultSettings,
@@ -382,7 +390,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
382390 maximumResetFrameCount: Int = 200 ,
383391 resetFrameCounterWindow: TimeAmount = . seconds( 30 ) ,
384392 maximumStreamErrorCount: Int = 200 ,
385- streamErrorCounterWindow: TimeAmount = . seconds( 30 )
393+ streamErrorCounterWindow: TimeAmount = . seconds( 30 ) ,
394+ maximumConnectionGlitches: Int = GlitchesMonitor . defaultMaximumGlitches
386395 ) {
387396 self . stateMachine = HTTP2ConnectionStateMachine (
388397 role: . init( mode) ,
@@ -408,6 +417,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
408417 self . tolerateImpossibleStateTransitionsInDebugMode = tolerateImpossibleStateTransitionsInDebugMode
409418 self . inboundStreamMultiplexerState = . uninitializedLegacy
410419 self . maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
420+ self . glitchesMonitor = GlitchesMonitor ( maximumGlitches: maximumConnectionGlitches)
411421 }
412422
413423 public func handlerAdded( context: ChannelHandlerContext ) {
@@ -600,32 +610,41 @@ extension NIOHTTP2Handler {
600610 self . inboundConnectionErrorTriggered (
601611 context: context,
602612 underlyingError: NIOHTTP2Errors . unableToParseFrame ( ) ,
603- reason: code
613+ reason: code,
614+ isMisbehavingPeer: false
604615 )
605616 return nil
606617 } catch is NIOHTTP2Errors . BadClientMagic {
607618 self . inboundConnectionErrorTriggered (
608619 context: context,
609620 underlyingError: NIOHTTP2Errors . badClientMagic ( ) ,
610- reason: . protocolError
621+ reason: . protocolError,
622+ isMisbehavingPeer: false
611623 )
612624 return nil
613625 } catch is NIOHTTP2Errors . ExcessivelyLargeHeaderBlock {
614626 self . inboundConnectionErrorTriggered (
615627 context: context,
616628 underlyingError: NIOHTTP2Errors . excessivelyLargeHeaderBlock ( ) ,
617- reason: . protocolError
629+ reason: . protocolError,
630+ isMisbehavingPeer: false
618631 )
619632 return nil
620633 } catch is NIOHTTP2Errors . ExcessiveContinuationFrames {
621634 self . inboundConnectionErrorTriggered (
622635 context: context,
623636 underlyingError: NIOHTTP2Errors . excessiveContinuationFrames ( ) ,
624- reason: . enhanceYourCalm
637+ reason: . enhanceYourCalm,
638+ isMisbehavingPeer: false
625639 )
626640 return nil
627641 } catch {
628- self . inboundConnectionErrorTriggered ( context: context, underlyingError: error, reason: . internalError)
642+ self . inboundConnectionErrorTriggered (
643+ context: context,
644+ underlyingError: error,
645+ reason: . internalError,
646+ isMisbehavingPeer: false
647+ )
629648 return nil
630649 }
631650 }
@@ -733,6 +752,7 @@ extension NIOHTTP2Handler {
733752 }
734753
735754 self . processDoSRisk ( frame, result: & result)
755+ self . processGlitches ( result: & result)
736756 self . processStateChange ( result. effect)
737757
738758 let returnValue : FrameProcessResult
@@ -744,9 +764,14 @@ extension NIOHTTP2Handler {
744764 case . ignoreFrame:
745765 // Frame is good but no action needs to be taken.
746766 returnValue = . continue
747- case . connectionError( let underlyingError, let errorCode) :
767+ case . connectionError( let underlyingError, let errorCode, let isMisbehavingPeer ) :
748768 // We should stop parsing on received connection errors, the connection is going away anyway.
749- self . inboundConnectionErrorTriggered ( context: context, underlyingError: underlyingError, reason: errorCode)
769+ self . inboundConnectionErrorTriggered (
770+ context: context,
771+ underlyingError: underlyingError,
772+ reason: errorCode,
773+ isMisbehavingPeer: isMisbehavingPeer
774+ )
750775 returnValue = . stop
751776 case . streamError( let streamID, let underlyingError, let errorCode) :
752777 // We can continue parsing on stream errors in most cases, the frame is just ignored.
@@ -770,15 +795,20 @@ extension NIOHTTP2Handler {
770795 private func inboundConnectionErrorTriggered(
771796 context: ChannelHandlerContext ,
772797 underlyingError: Error ,
773- reason: HTTP2ErrorCode
798+ reason: HTTP2ErrorCode ,
799+ isMisbehavingPeer: Bool
774800 ) {
775801 // A connection error brings the entire connection down. We attempt to write a GOAWAY frame, and then report this
776802 // error. It's possible that we'll be unable to write the GOAWAY frame, but that also just logs the error.
777803 // Because we don't know what data the user handled before we got this, we propose that they may have seen all of it.
778804 // The user may choose to fire a more specific error if they wish.
805+
806+ // If the peer is misbehaving, set the stream ID to the minimum allowed value (0).
807+ // This will cause all open streams for this connection to be immediately terminated.
808+ let streamID = isMisbehavingPeer ? HTTP2StreamID ( 0 ) : . maxID
779809 let goAwayFrame = HTTP2Frame (
780810 streamID: . rootStream,
781- payload: . goAway( lastStreamID: . maxID , errorCode: reason, opaqueData: nil )
811+ payload: . goAway( lastStreamID: streamID , errorCode: reason, opaqueData: nil )
782812 )
783813 self . writeUnbufferedFrame ( context: context, frame: goAwayFrame)
784814 self . flushIfNecessary ( context: context)
@@ -818,7 +848,29 @@ extension NIOHTTP2Handler {
818848 ( )
819849 }
820850 } catch {
821- result. result = StateMachineResult . connectionError ( underlyingError: error, type: . enhanceYourCalm)
851+ result. result = StateMachineResult . connectionError (
852+ underlyingError: error,
853+ type: . enhanceYourCalm,
854+ isMisbehavingPeer: true
855+ )
856+ result. effect = nil
857+ }
858+ }
859+
860+ private func processGlitches( result: inout StateMachineResultWithEffect ) {
861+ do {
862+ switch result. result {
863+ case . streamError:
864+ try self . glitchesMonitor. processStreamError ( )
865+ case . succeed, . ignoreFrame, . connectionError:
866+ ( )
867+ }
868+ } catch {
869+ result. result = . connectionError(
870+ underlyingError: error,
871+ type: . enhanceYourCalm,
872+ isMisbehavingPeer: true
873+ )
822874 result. effect = nil
823875 }
824876 }
@@ -894,7 +946,12 @@ extension NIOHTTP2Handler {
894946 }
895947 } catch let error where error is NIOHTTP2Errors . ExcessiveOutboundFrameBuffering {
896948 self . inboundStreamMultiplexer? . processedFrame ( frame)
897- self . inboundConnectionErrorTriggered ( context: context, underlyingError: error, reason: . enhanceYourCalm)
949+ self . inboundConnectionErrorTriggered (
950+ context: context,
951+ underlyingError: error,
952+ reason: . enhanceYourCalm,
953+ isMisbehavingPeer: false
954+ )
898955 } catch {
899956 self . inboundStreamMultiplexer? . processedFrame ( frame)
900957 promise? . fail ( error)
@@ -968,7 +1025,7 @@ extension NIOHTTP2Handler {
9681025 switch result. result {
9691026 case . ignoreFrame:
9701027 preconditionFailure ( " Cannot be asked to ignore outbound frames. " )
971- case . connectionError( let underlyingError, _) :
1028+ case . connectionError( let underlyingError, _, _ ) :
9721029 self . outboundConnectionErrorTriggered ( context: context, promise: promise, underlyingError: underlyingError)
9731030 return
9741031 case . streamError( let streamID, let underlyingError, _) :
@@ -1330,6 +1387,7 @@ extension NIOHTTP2Handler {
13301387 maximumBufferedControlFrames: connectionConfiguration. maximumBufferedControlFrames,
13311388 maximumSequentialContinuationFrames: connectionConfiguration. maximumSequentialContinuationFrames,
13321389 maximumRecentlyResetStreams: connectionConfiguration. maximumRecentlyResetStreams,
1390+ maximumConnectionGlitches: connectionConfiguration. maximumConnectionGlitches,
13331391 maximumResetFrameCount: streamConfiguration. streamResetFrameRateLimit. maximumCount,
13341392 resetFrameCounterWindow: streamConfiguration. streamResetFrameRateLimit. windowLength,
13351393 maximumStreamErrorCount: streamConfiguration. streamErrorRateLimit. maximumCount,
@@ -1362,6 +1420,7 @@ extension NIOHTTP2Handler {
13621420 maximumBufferedControlFrames: connectionConfiguration. maximumBufferedControlFrames,
13631421 maximumSequentialContinuationFrames: connectionConfiguration. maximumSequentialContinuationFrames,
13641422 maximumRecentlyResetStreams: connectionConfiguration. maximumRecentlyResetStreams,
1423+ maximumConnectionGlitches: connectionConfiguration. maximumConnectionGlitches,
13651424 maximumResetFrameCount: streamConfiguration. streamResetFrameRateLimit. maximumCount,
13661425 resetFrameCounterWindow: streamConfiguration. streamResetFrameRateLimit. windowLength,
13671426 maximumStreamErrorCount: streamConfiguration. streamErrorRateLimit. maximumCount,
@@ -1386,6 +1445,17 @@ extension NIOHTTP2Handler {
13861445 public var maximumBufferedControlFrames : Int = 10000
13871446 public var maximumSequentialContinuationFrames : Int = NIOHTTP2Handler . defaultMaximumSequentialContinuationFrames
13881447 public var maximumRecentlyResetStreams : Int = NIOHTTP2Handler . defaultMaximumRecentlyResetFrames
1448+
1449+ /// The maximum number of glitches that are allowed on a connection before it's forcefully closed.
1450+ ///
1451+ /// A glitch is defined as some suspicious event on a connection, i.e., similar to a DoS attack.
1452+ /// A running count of the number of glitches occurring on each connection will be kept.
1453+ /// When the number of glitches reaches this threshold, the connection will be closed.
1454+ ///
1455+ /// For more information, see the relevant presentation of the 2024 HTTP Workshop:
1456+ /// https://github.com/HTTPWorkshop/workshop2024/blob/main/talks/1.%20Security/glitches.pdf
1457+ public var maximumConnectionGlitches : Int = GlitchesMonitor . defaultMaximumGlitches
1458+
13891459 public init ( ) { }
13901460 }
13911461
0 commit comments