Skip to content

Commit 26a30d8

Browse files
jasnellsxa
authored andcommitted
quic: implement rate limiting for version nego and immediate close
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63267 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 0b534b5 commit 26a30d8

2 files changed

Lines changed: 53 additions & 13 deletions

File tree

src/quic/endpoint.cc

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -790,22 +790,30 @@ void Endpoint::SendRetry(const PathDescriptor& options) {
790790

791791
void Endpoint::SendVersionNegotiation(const PathDescriptor& options) {
792792
Debug(this, "Sending version negotiation on path %s", options);
793-
// While creating and sending a version negotiation packet does consume a
794-
// small amount of system resources, and while it is fairly trivial for a
795-
// malicious peer to force a version negotiation to be sent, these are more
796-
// trivial to create than the cryptographically generated retry and stateless
797-
// reset packets. If the packet is sent, then we'll at least increment the
798-
// version_negotiation_count statistic so that application code can keep an
799-
// eye on it.
793+
// A malicious peer can trivially force version negotiation packets by
794+
// sending packets with unsupported QUIC versions, potentially from
795+
// spoofed source addresses. Rate-limit per remote host to prevent
796+
// amplification attacks.
797+
const auto exceeds_limits = [&] {
798+
SocketAddressInfoTraits::Type* counts =
799+
addr_validation_lru_.Peek(options.remote_address);
800+
auto count = counts != nullptr ? counts->version_negotiation_count : 0;
801+
return count >= kMaxVersionNegotiations;
802+
};
803+
804+
if (exceeds_limits()) {
805+
Debug(this, "Version negotiation rate limit exceeded for %s",
806+
options.remote_address);
807+
return;
808+
}
809+
800810
auto packet = Packet::CreateVersionNegotiationPacket(*this, options);
801811
if (packet) {
812+
addr_validation_lru_.Upsert(options.remote_address)
813+
->version_negotiation_count++;
802814
STAT_INCREMENT(Stats, version_negotiation_count);
803815
Send(std::move(packet));
804816
}
805-
806-
// If creating the packet is unsuccessful, we just drop things on the floor.
807-
// It's not worth committing any further resources to this one packet. We
808-
// might want to log the failure at some point tho.
809817
}
810818

811819
bool Endpoint::SendStatelessReset(const PathDescriptor& options,
@@ -847,11 +855,27 @@ void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options,
847855
"Sending immediate connection close on path %s with reason %s",
848856
options,
849857
reason);
850-
// While it is possible for a malicious peer to cause us to create a large
851-
// number of these, generating them is fairly trivial.
858+
// A malicious peer can trigger immediate connection close packets by
859+
// sending Initial packets with invalid tokens or when the server is
860+
// busy. Rate-limit per remote host to prevent amplification attacks.
861+
const auto exceeds_limits = [&] {
862+
SocketAddressInfoTraits::Type* counts =
863+
addr_validation_lru_.Peek(options.remote_address);
864+
auto count = counts != nullptr ? counts->immediate_close_count : 0;
865+
return count >= kMaxImmediateCloses;
866+
};
867+
868+
if (exceeds_limits()) {
869+
Debug(this, "Immediate connection close rate limit exceeded for %s",
870+
options.remote_address);
871+
return;
872+
}
873+
852874
auto packet =
853875
Packet::CreateImmediateConnectionClosePacket(*this, options, reason);
854876
if (packet) {
877+
addr_validation_lru_.Upsert(options.remote_address)
878+
->immediate_close_count++;
855879
STAT_INCREMENT(Stats, immediate_close_count);
856880
Send(std::move(packet));
857881
}

src/quic/endpoint.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
3232
static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10;
3333
static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10;
3434

35+
// Maximum number of version negotiation packets that will be sent to a
36+
// given remote host within the LRU tracking window. Version negotiation
37+
// packets are cheap to generate but can be used as an amplification
38+
// vector with spoofed source addresses.
39+
// TODO(@jasnell): Consider making this configurable via Endpoint::Options.
40+
static constexpr uint64_t kMaxVersionNegotiations = 10;
41+
42+
// Maximum number of immediate connection close packets that will be sent
43+
// to a given remote host within the LRU tracking window. These are sent
44+
// when the server is busy or a token is invalid — a malicious peer could
45+
// trigger a large number of them.
46+
// TODO(@jasnell): Consider making this configurable via Endpoint::Options.
47+
static constexpr uint64_t kMaxImmediateCloses = 10;
48+
3549
// Endpoint configuration options
3650
struct Options final : public MemoryRetainer {
3751
// The local socket address to which the UDP port will be bound. The port
@@ -397,6 +411,8 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
397411
size_t active_connections;
398412
size_t reset_count;
399413
size_t retry_count;
414+
size_t version_negotiation_count;
415+
size_t immediate_close_count;
400416
uint64_t timestamp;
401417
bool validated;
402418
};

0 commit comments

Comments
 (0)