Skip to content

Commit 350da87

Browse files
authored
Merge commit from fork
* Enforce the maximum number of RST frames that can be sent in window of time Motivation: A remote peer might be able to trigger an instance to generate and send RST frames by sending invalid frames on an existing stream. This can cause high resource usage and so might be abused by a remote peer. Modifications: Limit the number of RSTs that we allow to be generated and so send in a specific time window. If this limit is reached a GO_AWAY frame is send and the connection be closed. Result: Fix high resource usage that can be caused by a remote peer by trigger RST frames * Adjust testing * Address comments
1 parent 817bab1 commit 350da87

6 files changed

Lines changed: 361 additions & 12 deletions

File tree

codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
109109
private boolean autoAckPingFrame = true;
110110
private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;
111111
private int maxConsecutiveEmptyFrames = 2;
112-
private Integer maxRstFramesPerWindow;
113-
private int secondsPerWindow = 30;
114-
112+
private Integer maxDecodedRstFramesPerWindow;
113+
private int maxDecodedRstFramesSecondsPerWindow = 30;
114+
private Integer maxEncodedRstFramesPerWindow;
115+
private int maxEncodedRstFramesSecondsPerWindow = 30;
115116
/**
116117
* Sets the {@link Http2Settings} to use for the initial connection settings exchange.
117118
*/
@@ -444,9 +445,24 @@ protected B decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyF
444445
*/
445446
protected B decoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int secondsPerWindow) {
446447
enforceNonCodecConstraints("decoderEnforceMaxRstFramesPerWindow");
447-
this.maxRstFramesPerWindow = checkPositiveOrZero(
448+
this.maxDecodedRstFramesPerWindow = checkPositiveOrZero(
449+
maxRstFramesPerWindow, "maxRstFramesPerWindow");
450+
this.maxDecodedRstFramesSecondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
451+
return self();
452+
}
453+
454+
/**
455+
* Sets the maximum number RST frames that are allowed per window before
456+
* the connection is closed. This allows to protect against the remote peer that will trigger us to generate a flood
457+
* of RST frames and so use up a lot of CPU.
458+
*
459+
* {@code 0} for any of the parameters means no protection should be applied.
460+
*/
461+
protected B encoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int secondsPerWindow) {
462+
enforceNonCodecConstraints("decoderEnforceMaxRstFramesPerWindow");
463+
this.maxEncodedRstFramesPerWindow = checkPositiveOrZero(
448464
maxRstFramesPerWindow, "maxRstFramesPerWindow");
449-
this.secondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
465+
this.maxEncodedRstFramesSecondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
450466
return self();
451467
}
452468

@@ -571,6 +587,21 @@ private T buildFromConnection(Http2Connection connection) {
571587
if (maxQueuedControlFrames != 0) {
572588
encoder = new Http2ControlFrameLimitEncoder(encoder, maxQueuedControlFrames);
573589
}
590+
final int maxEncodedRstFrames;
591+
if (maxEncodedRstFramesPerWindow == null) {
592+
// Only enable by default on the server.
593+
if (isServer()) {
594+
maxEncodedRstFrames = DEFAULT_MAX_RST_FRAMES_PER_CONNECTION_FOR_SERVER;
595+
} else {
596+
maxEncodedRstFrames = 0;
597+
}
598+
} else {
599+
maxEncodedRstFrames = maxEncodedRstFramesPerWindow;
600+
}
601+
if (maxEncodedRstFrames > 0 && maxEncodedRstFramesSecondsPerWindow > 0) {
602+
encoder = new Http2MaxRstFrameLimitEncoder(
603+
encoder, maxEncodedRstFrames, maxEncodedRstFramesSecondsPerWindow);
604+
}
574605
if (encoderEnforceMaxConcurrentStreams) {
575606
if (connection.isServer()) {
576607
encoder.close();
@@ -592,19 +623,19 @@ private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder
592623
if (maxConsecutiveEmptyDataFrames > 0) {
593624
decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
594625
}
595-
final int maxRstFrames;
596-
if (maxRstFramesPerWindow == null) {
626+
final int maxDecodedRstFrames;
627+
if (maxDecodedRstFramesPerWindow == null) {
597628
// Only enable by default on the server.
598629
if (isServer()) {
599-
maxRstFrames = DEFAULT_MAX_RST_FRAMES_PER_CONNECTION_FOR_SERVER;
630+
maxDecodedRstFrames = DEFAULT_MAX_RST_FRAMES_PER_CONNECTION_FOR_SERVER;
600631
} else {
601-
maxRstFrames = 0;
632+
maxDecodedRstFrames = 0;
602633
}
603634
} else {
604-
maxRstFrames = maxRstFramesPerWindow;
635+
maxDecodedRstFrames = maxDecodedRstFramesPerWindow;
605636
}
606-
if (maxRstFrames > 0 && secondsPerWindow > 0) {
607-
decoder = new Http2MaxRstFrameDecoder(decoder, maxRstFrames, secondsPerWindow);
637+
if (maxDecodedRstFrames > 0 && maxDecodedRstFramesSecondsPerWindow > 0) {
638+
decoder = new Http2MaxRstFrameDecoder(decoder, maxDecodedRstFrames, maxDecodedRstFramesSecondsPerWindow);
608639
}
609640
final T handler;
610641
try {

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
2020

21+
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
22+
2123
/**
2224
* Builder which builds {@link Http2ConnectionHandler} objects.
2325
*/
@@ -122,6 +124,12 @@ public Http2ConnectionHandlerBuilder decoderEnforceMaxRstFramesPerWindow(
122124
return super.decoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
123125
}
124126

127+
@Override
128+
public Http2ConnectionHandlerBuilder encoderEnforceMaxRstFramesPerWindow(
129+
int maxRstFramesPerWindow, int secondsPerWindow) {
130+
return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
131+
}
132+
125133
@Override
126134
public Http2ConnectionHandler build() {
127135
return super.build();

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ public Http2FrameCodecBuilder decoderEnforceMaxRstFramesPerWindow(
197197
return super.decoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
198198
}
199199

200+
@Override
201+
public Http2FrameCodecBuilder encoderEnforceMaxRstFramesPerWindow(
202+
int maxRstFramesPerWindow, int secondsPerWindow) {
203+
return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
204+
}
205+
200206
/**
201207
* Build a {@link Http2FrameCodec} object.
202208
*/
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2025 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5+
* "License"); you may not use this file except in compliance with the License. You may obtain a
6+
* copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
package io.netty.handler.codec.http2;
16+
17+
import io.netty.channel.ChannelFuture;
18+
import io.netty.channel.ChannelHandlerContext;
19+
import io.netty.channel.ChannelPromise;
20+
import io.netty.util.internal.logging.InternalLogger;
21+
import io.netty.util.internal.logging.InternalLoggerFactory;
22+
23+
import java.util.concurrent.TimeUnit;
24+
25+
/**
26+
* {@link DecoratingHttp2ConnectionEncoder} which guards against a remote peer that will trigger a massive amount
27+
* of RST frames on an existing connection.
28+
* This encoder will tear-down the connection once we reached the configured limit to reduce the risk of DDOS.
29+
*/
30+
final class Http2MaxRstFrameLimitEncoder extends DecoratingHttp2ConnectionEncoder {
31+
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2MaxRstFrameLimitEncoder.class);
32+
33+
private final long nanosPerWindow;
34+
private final int maxRstFramesPerWindow;
35+
private long lastRstFrameNano = System.nanoTime();
36+
private int sendRstInWindow;
37+
private Http2LifecycleManager lifecycleManager;
38+
39+
Http2MaxRstFrameLimitEncoder(Http2ConnectionEncoder delegate, int maxRstFramesPerWindow, int secondsPerWindow) {
40+
super(delegate);
41+
this.maxRstFramesPerWindow = maxRstFramesPerWindow;
42+
this.nanosPerWindow = TimeUnit.SECONDS.toNanos(secondsPerWindow);
43+
}
44+
45+
@Override
46+
public void lifecycleManager(Http2LifecycleManager lifecycleManager) {
47+
this.lifecycleManager = lifecycleManager;
48+
super.lifecycleManager(lifecycleManager);
49+
}
50+
51+
@Override
52+
public ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
53+
ChannelPromise promise) {
54+
ChannelFuture future = super.writeRstStream(ctx, streamId, errorCode, promise);
55+
if (countRstFrameErrorCode(errorCode)) {
56+
long currentNano = System.nanoTime();
57+
if (currentNano - lastRstFrameNano >= nanosPerWindow) {
58+
lastRstFrameNano = currentNano;
59+
sendRstInWindow = 1;
60+
} else {
61+
sendRstInWindow++;
62+
if (sendRstInWindow > maxRstFramesPerWindow) {
63+
Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
64+
"Maximum number %d of RST frames frames reached within %d seconds", maxRstFramesPerWindow,
65+
TimeUnit.NANOSECONDS.toSeconds(nanosPerWindow));
66+
67+
logger.debug("{} Maximum number {} of RST frames reached within {} seconds, " +
68+
"closing connection with {} error", ctx.channel(), maxRstFramesPerWindow,
69+
TimeUnit.NANOSECONDS.toSeconds(nanosPerWindow), exception.error(),
70+
exception);
71+
// First notify the Http2LifecycleManager and then close the connection.
72+
lifecycleManager.onError(ctx, true, exception);
73+
ctx.close();
74+
}
75+
}
76+
}
77+
78+
return future;
79+
}
80+
81+
private boolean countRstFrameErrorCode(long errorCode) {
82+
// Don't count CANCEL and NO_ERROR as these might be ok.
83+
return errorCode != Http2Error.CANCEL.code() && errorCode != Http2Error.NO_ERROR.code();
84+
}
85+
}

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ public Http2MultiplexCodecBuilder decoderEnforceMaxRstFramesPerWindow(
215215
return super.decoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
216216
}
217217

218+
@Override
219+
public Http2MultiplexCodecBuilder encoderEnforceMaxRstFramesPerWindow(
220+
int maxRstFramesPerWindow, int secondsPerWindow) {
221+
return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
222+
}
223+
218224
@Override
219225
public Http2MultiplexCodec build() {
220226
Http2FrameWriter frameWriter = this.frameWriter;

0 commit comments

Comments
 (0)