Skip to content

Commit 7fd5f51

Browse files
ikhoonBue-von-hon
authored andcommitted
Limit max reset frames to mitigate HTTP/2 RST floods (line#5232)
Motivation: To mitigate against the "HTTP/2 Rapid Reset" attack, it is recommended that HTTP/2 servers should close connections that exceed the concurrent stream limit. Reference: - https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/ - https://www.cve.org/CVERecord?id=CVE-2023-44487 - netty/netty@58f75f6#diff-82f568a075ff63e9727ce8622f3a2b1553099182edf1fd0b4f857226252b05adR47 Modifications: - Add `ServerBuilder.http2MaxRestFramesPerWindow()` option and `-Dcom.linecorp.armeria.defaultHttp2MaxResetFramesPerMinute<integer>` property to limit the maximum allowed RST frames. - If not set, 400 RST frames per minute are allowed by default. - Bump Netty version to 4.1.100 from 4.1.96 Result: You can now protect your server against DDOS caused by RST floods. ```java Server .builder() .http2MaxResetFramesPerWindow(100, 10) .build(); ```
1 parent bd17de5 commit 7fd5f51

12 files changed

+222
-4
lines changed

core/src/main/java/com/linecorp/armeria/common/DefaultFlagsProvider.java

+6
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ final class DefaultFlagsProvider implements FlagsProvider {
7878
// parameter values, thus anything greater than 0x7FFFFFFF will break them or make them unhappy.
7979
static final long DEFAULT_HTTP2_MAX_STREAMS_PER_CONNECTION = Integer.MAX_VALUE;
8080
static final long DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE = 8192L; // from Netty default maxHeaderSize
81+
static final int DEFAULT_HTTP2_MAX_RESET_FRAMES_PER_MINUTE = 400; // Netty default is 200 for 30 seconds
8182
static final String DEFAULT_BACKOFF_SPEC = "exponential=200:10000,jitter=0.2";
8283
static final int DEFAULT_MAX_TOTAL_ATTEMPTS = 10;
8384
static final long DEFAULT_REQUEST_AUTO_ABORT_DELAY_MILLIS = 0; // No delay.
@@ -322,6 +323,11 @@ public Long defaultHttp2MaxHeaderListSize() {
322323
return DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE;
323324
}
324325

326+
@Override
327+
public Integer defaultHttp2MaxResetFramesPerMinute() {
328+
return DEFAULT_HTTP2_MAX_RESET_FRAMES_PER_MINUTE;
329+
}
330+
325331
@Override
326332
public String defaultBackoffSpec() {
327333
return DEFAULT_BACKOFF_SPEC;

core/src/main/java/com/linecorp/armeria/common/Flags.java

+20
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ private static boolean validateTransportType(TransportType transportType, String
285285
getValue(FlagsProvider::defaultHttp2MaxHeaderListSize, "defaultHttp2MaxHeaderListSize",
286286
value -> value > 0 && value <= 0xFFFFFFFFL);
287287

288+
private static final int DEFAULT_HTTP2_MAX_RESET_FRAMES_PER_MINUTE =
289+
getValue(FlagsProvider::defaultHttp2MaxResetFramesPerMinute,
290+
"defaultHttp2MaxResetFramesPerMinute", value -> value >= 0);
291+
288292
private static final int DEFAULT_MAX_HTTP1_INITIAL_LINE_LENGTH =
289293
getValue(FlagsProvider::defaultHttp1MaxInitialLineLength, "defaultHttp1MaxInitialLineLength",
290294
value -> value >= 0);
@@ -1052,6 +1056,22 @@ public static long defaultHttp2MaxHeaderListSize() {
10521056
return DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE;
10531057
}
10541058

1059+
/**
1060+
* Returns the default maximum number of RST frames that are allowed per window before the connection is
1061+
* closed. This allows to protect against the remote peer flooding us with such frames and using up a lot
1062+
* of CPU. Note that this flag has no effect if a user specified the value explicitly via
1063+
* {@link ServerBuilder#http2MaxResetFramesPerWindow(int, int)}.
1064+
*
1065+
* <p>The default value of this flag is
1066+
* {@value DefaultFlagsProvider#DEFAULT_HTTP2_MAX_RESET_FRAMES_PER_MINUTE}.
1067+
* Specify the {@code -Dcom.linecorp.armeria.defaultHttp2MaxResetFramesPerMinute=<integer>} JVM option
1068+
* to override the default value. {@code 0} means no protection should be applied.
1069+
*/
1070+
@UnstableApi
1071+
public static int defaultHttp2MaxResetFramesPerMinute() {
1072+
return DEFAULT_HTTP2_MAX_RESET_FRAMES_PER_MINUTE;
1073+
}
1074+
10551075
/**
10561076
* Returns the {@linkplain Backoff#of(String) Backoff specification string} of the default {@link Backoff}
10571077
* returned by {@link Backoff#ofDefault()}. Note that this flag has no effect if a user specified the

core/src/main/java/com/linecorp/armeria/common/FlagsProvider.java

+16
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,22 @@ default Long defaultHttp2MaxHeaderListSize() {
711711
return null;
712712
}
713713

714+
/**
715+
* Returns the default maximum number of RST frames that are allowed per window before the connection is
716+
* closed. This allows to protect against the remote peer flooding us with such frames and using up a lot
717+
* of CPU. Note that this flag has no effect if a user specified the value explicitly via
718+
* {@link ServerBuilder#http2MaxResetFramesPerWindow(int, int)}.
719+
*
720+
* <p>The default value of this flag is
721+
* {@value DefaultFlagsProvider#DEFAULT_HTTP2_MAX_RESET_FRAMES_PER_MINUTE}.
722+
* Specify the {@code -Dcom.linecorp.armeria.defaultHttp2MaxResetFramesPerMinute=<integer>} JVM option
723+
* to override the default value. {@code 0} means no protection should be applied.
724+
*/
725+
@Nullable
726+
default Integer defaultHttp2MaxResetFramesPerMinute() {
727+
return null;
728+
}
729+
714730
/**
715731
* Returns the {@linkplain Backoff#of(String) Backoff specification string} of the default {@link Backoff}
716732
* returned by {@link Backoff#ofDefault()}. Note that this flag has no effect if a user specified the

core/src/main/java/com/linecorp/armeria/common/SystemPropertyFlagsProvider.java

+5
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ public Long defaultHttp2MaxHeaderListSize() {
298298
return getLong("defaultHttp2MaxHeaderListSize");
299299
}
300300

301+
@Override
302+
public Integer defaultHttp2MaxResetFramesPerMinute() {
303+
return getInt("defaultHttp2MaxResetFramesPerMinute");
304+
}
305+
301306
@Override
302307
public String defaultBackoffSpec() {
303308
return getNormalized("defaultBackoffSpec");

core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java

+17-2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ final class DefaultServerConfig implements ServerConfig {
8585
private final long http2MaxStreamsPerConnection;
8686
private final int http2MaxFrameSize;
8787
private final long http2MaxHeaderListSize;
88+
private final int http2MaxResetFramesPerWindow;
89+
private final int http2MaxResetFramesWindowSeconds;
8890
private final int http1MaxInitialLineLength;
8991
private final int http1MaxHeaderSize;
9092
private final int http1MaxChunkSize;
@@ -129,8 +131,9 @@ final class DefaultServerConfig implements ServerConfig {
129131
long maxConnectionAgeMillis,
130132
int maxNumRequestsPerConnection, long connectionDrainDurationMicros,
131133
int http2InitialConnectionWindowSize, int http2InitialStreamWindowSize,
132-
long http2MaxStreamsPerConnection, int http2MaxFrameSize,
133-
long http2MaxHeaderListSize, int http1MaxInitialLineLength, int http1MaxHeaderSize,
134+
long http2MaxStreamsPerConnection, int http2MaxFrameSize, long http2MaxHeaderListSize,
135+
int http2MaxResetFramesPerWindow, int http2MaxResetFramesWindowSeconds,
136+
int http1MaxInitialLineLength, int http1MaxHeaderSize,
134137
int http1MaxChunkSize, Duration gracefulShutdownQuietPeriod, Duration gracefulShutdownTimeout,
135138
BlockingTaskExecutor blockingTaskExecutor,
136139
MeterRegistry meterRegistry, int proxyProtocolMaxTlvSize,
@@ -171,6 +174,8 @@ final class DefaultServerConfig implements ServerConfig {
171174
this.http2MaxStreamsPerConnection = http2MaxStreamsPerConnection;
172175
this.http2MaxFrameSize = http2MaxFrameSize;
173176
this.http2MaxHeaderListSize = http2MaxHeaderListSize;
177+
this.http2MaxResetFramesPerWindow = http2MaxResetFramesPerWindow;
178+
this.http2MaxResetFramesWindowSeconds = http2MaxResetFramesWindowSeconds;
174179
this.http1MaxInitialLineLength = validateNonNegative(
175180
http1MaxInitialLineLength, "http1MaxInitialLineLength");
176181
this.http1MaxHeaderSize = validateNonNegative(
@@ -568,6 +573,16 @@ public long http2MaxHeaderListSize() {
568573
return http2MaxHeaderListSize;
569574
}
570575

576+
@Override
577+
public int http2MaxResetFramesPerWindow() {
578+
return http2MaxResetFramesPerWindow;
579+
}
580+
581+
@Override
582+
public int http2MaxResetFramesWindowSeconds() {
583+
return http2MaxResetFramesWindowSeconds;
584+
}
585+
571586
@Override
572587
public Duration gracefulShutdownQuietPeriod() {
573588
return gracefulShutdownQuietPeriod;

core/src/main/java/com/linecorp/armeria/server/Http2ServerConnectionHandlerBuilder.java

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ final class Http2ServerConnectionHandlerBuilder
4242
// Disable graceful shutdown timeout in a super class. Server-side HTTP/2 graceful shutdown is
4343
// handled by Armeria's HTTP/2 server handler.
4444
gracefulShutdownTimeoutMillis(-1);
45+
decoderEnforceMaxRstFramesPerWindow(config.http2MaxResetFramesPerWindow(),
46+
config.http2MaxResetFramesWindowSeconds());
4547
}
4648

4749
@Override

core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ public final class ServerBuilder implements TlsSetters, ServiceConfigsBuilder {
230230
private long unhandledExceptionsReportIntervalMillis =
231231
Flags.defaultUnhandledExceptionsReportIntervalMillis();
232232
private final List<ShutdownSupport> shutdownSupports = new ArrayList<>();
233+
private int http2MaxResetFramesPerWindow = Flags.defaultHttp2MaxResetFramesPerMinute();
234+
private int http2MaxResetFramesWindowSeconds = 60;
233235

234236
ServerBuilder() {
235237
// Set the default host-level properties.
@@ -771,6 +773,26 @@ public ServerBuilder http2MaxStreamsPerConnection(long http2MaxStreamsPerConnect
771773
return this;
772774
}
773775

776+
/**
777+
* Sets the maximum number of RST frames that are allowed per window before the connection is closed. This
778+
* allows to protect against the remote peer flooding us with such frames and using up a lot of CPU.
779+
* Defaults to {@link Flags#defaultHttp2MaxResetFramesPerMinute()}.
780+
*
781+
* <p>Note that {@code 0} for any of the parameters means no protection should be applied.
782+
*/
783+
@UnstableApi
784+
public ServerBuilder http2MaxResetFramesPerWindow(int http2MaxResetFramesPerWindow,
785+
int http2MaxResetFramesWindowSeconds) {
786+
checkArgument(http2MaxResetFramesPerWindow >= 0, "http2MaxResetFramesPerWindow: %s (expected: >= 0)",
787+
http2MaxResetFramesPerWindow);
788+
checkArgument(http2MaxResetFramesWindowSeconds >= 0,
789+
"http2MaxResetFramesWindowSeconds: %s (expected: >= 0)",
790+
http2MaxResetFramesWindowSeconds);
791+
this.http2MaxResetFramesPerWindow = http2MaxResetFramesPerWindow;
792+
this.http2MaxResetFramesWindowSeconds = http2MaxResetFramesWindowSeconds;
793+
return this;
794+
}
795+
774796
/**
775797
* Sets the maximum size of HTTP/2 frame that can be received. Defaults to
776798
* {@link Flags#defaultHttp2MaxFrameSize()}.
@@ -2168,7 +2190,9 @@ ports, setSslContextIfAbsent(defaultVirtualHost, defaultSslContext),
21682190
maxNumRequestsPerConnection,
21692191
connectionDrainDurationMicros, http2InitialConnectionWindowSize,
21702192
http2InitialStreamWindowSize, http2MaxStreamsPerConnection,
2171-
http2MaxFrameSize, http2MaxHeaderListSize, http1MaxInitialLineLength, http1MaxHeaderSize,
2193+
http2MaxFrameSize, http2MaxHeaderListSize,
2194+
http2MaxResetFramesPerWindow, http2MaxResetFramesWindowSeconds,
2195+
http1MaxInitialLineLength, http1MaxHeaderSize,
21722196
http1MaxChunkSize, gracefulShutdownQuietPeriod, gracefulShutdownTimeout,
21732197
blockingTaskExecutor,
21742198
meterRegistry, proxyProtocolMaxTlvSize, channelOptions, newChildChannelOptions,

core/src/main/java/com/linecorp/armeria/server/ServerConfig.java

+14
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,20 @@ public interface ServerConfig {
213213
*/
214214
long http2MaxHeaderListSize();
215215

216+
/**
217+
* Returns the maximum number of RST frames that are allowed per
218+
* {@link #http2MaxResetFramesWindowSeconds()}.
219+
*/
220+
@UnstableApi
221+
int http2MaxResetFramesPerWindow();
222+
223+
/**
224+
* Returns the number of seconds during which {@link #http2MaxResetFramesPerWindow()} RST frames are
225+
* allowed.
226+
*/
227+
@UnstableApi
228+
int http2MaxResetFramesWindowSeconds();
229+
216230
/**
217231
* Returns the number of milliseconds to wait for active requests to go end before shutting down.
218232
* {@code 0} means the server will stop right away without waiting.

core/src/main/java/com/linecorp/armeria/server/UpdatableServerConfig.java

+10
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@ public long http2MaxHeaderListSize() {
220220
return delegate.http2MaxHeaderListSize();
221221
}
222222

223+
@Override
224+
public int http2MaxResetFramesPerWindow() {
225+
return delegate.http2MaxResetFramesPerWindow();
226+
}
227+
228+
@Override
229+
public int http2MaxResetFramesWindowSeconds() {
230+
return delegate.http2MaxResetFramesWindowSeconds();
231+
}
232+
223233
@Override
224234
public Duration gracefulShutdownQuietPeriod() {
225235
return delegate.gracefulShutdownQuietPeriod();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2023 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a 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
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.server;
18+
19+
import static com.google.common.collect.ImmutableList.toImmutableList;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.awaitility.Awaitility.await;
22+
23+
import java.util.List;
24+
import java.util.concurrent.CompletableFuture;
25+
import java.util.stream.IntStream;
26+
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.extension.RegisterExtension;
29+
30+
import com.spotify.futures.CompletableFutures;
31+
32+
import com.linecorp.armeria.client.ClientFactory;
33+
import com.linecorp.armeria.client.CountingConnectionPoolListener;
34+
import com.linecorp.armeria.client.WebClient;
35+
import com.linecorp.armeria.common.AggregatedHttpResponse;
36+
import com.linecorp.armeria.common.HttpMethod;
37+
import com.linecorp.armeria.common.HttpObject;
38+
import com.linecorp.armeria.common.HttpRequest;
39+
import com.linecorp.armeria.common.HttpResponse;
40+
import com.linecorp.armeria.common.RequestHeaders;
41+
import com.linecorp.armeria.common.SessionProtocol;
42+
import com.linecorp.armeria.common.stream.StreamMessage;
43+
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
44+
45+
class MaxResetFramesTest {
46+
@RegisterExtension
47+
static final ServerExtension server = new ServerExtension() {
48+
@Override
49+
protected void configure(ServerBuilder sb) {
50+
sb.idleTimeoutMillis(0);
51+
sb.http2MaxResetFramesPerWindow(10, 60);
52+
sb.service("/", (ctx, req) -> {
53+
return HttpResponse.of(req.aggregate().thenApply(unused -> HttpResponse.of(200)));
54+
});
55+
}
56+
};
57+
58+
@Test
59+
void shouldCloseConnectionWhenExceedingMaxResetFrames() {
60+
final CountingConnectionPoolListener listener = new CountingConnectionPoolListener();
61+
try (ClientFactory factory = ClientFactory.builder()
62+
.connectionPoolListener(listener)
63+
.idleTimeoutMillis(0)
64+
.build()) {
65+
final WebClient client = WebClient.builder(server.uri(SessionProtocol.H2C))
66+
.factory(factory)
67+
.build();
68+
final List<CompletableFuture<AggregatedHttpResponse>> futures =
69+
IntStream.range(0, 11)
70+
.mapToObj(unused -> HttpRequest.of(RequestHeaders.of(HttpMethod.POST, "/"),
71+
StreamMessage.of(InvalidHttpObject.INSTANCE)))
72+
.map(client::execute)
73+
.map(HttpResponse::aggregate)
74+
.collect(toImmutableList());
75+
76+
CompletableFutures.successfulAsList(futures, cause -> null).join();
77+
assertThat(listener.opened()).isEqualTo(1);
78+
await().untilAsserted(() -> assertThat(listener.closed()).isEqualTo(1));
79+
}
80+
}
81+
82+
/**
83+
* {@link WebClient} resets a stream when it receives an invalid {@link HttpObject} from
84+
* {@link HttpRequest}.
85+
*/
86+
private enum InvalidHttpObject implements HttpObject {
87+
88+
INSTANCE;
89+
90+
@Override
91+
public boolean isEndOfStream() {
92+
return false;
93+
}
94+
}
95+
}

core/src/test/java/com/linecorp/armeria/server/ServerBuilderTest.java

+11
Original file line numberDiff line numberDiff line change
@@ -705,4 +705,15 @@ void multipleDomainSocketAddresses() {
705705
new ServerPort(DomainSocketAddress.of("/tmp/bar"),
706706
SessionProtocol.HTTP, SessionProtocol.HTTPS));
707707
}
708+
709+
@Test
710+
void httpMaxResetFramesPerMinute() {
711+
final ServerConfig config = Server.builder()
712+
.service("/", (ctx, req) -> HttpResponse.of(HttpStatus.OK))
713+
.http2MaxResetFramesPerWindow(99, 2)
714+
.build()
715+
.config();
716+
assertThat(config.http2MaxResetFramesPerWindow()).isEqualTo(99);
717+
assertThat(config.http2MaxResetFramesWindowSeconds()).isEqualTo(2);
718+
}
708719
}

dependencies.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ micrometer13 = "1.3.20"
8484
mockito = "4.11.0"
8585
monix = "3.4.1"
8686
munit = "0.7.29"
87-
netty = "4.1.96.Final"
87+
netty = "4.1.100.Final"
8888
netty-incubator-transport-native-io_uring = "0.0.21.Final"
8989
nexus-publish = "1.3.0"
9090
node-gradle-plugin = "5.0.0"

0 commit comments

Comments
 (0)