mirror of
https://github.com/PurpurMC/Purpur.git
synced 2026-02-17 08:27:43 +01:00
Rate limit packets incoming from players
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
From 16a706182d2cb208e6410a44bc1bdf2fb75aa9b6 Mon Sep 17 00:00:00 2001
|
||||
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
||||
Date: Mon, 11 Mar 2019 12:18:29 -0700
|
||||
Subject: [PATCH] Rate limit packets incoming from players
|
||||
|
||||
---
|
||||
.../com/destroystokyo/paper/PaperConfig.java | 12 ++
|
||||
.../paper/network/PacketLimiter.java | 135 ++++++++++++++++++
|
||||
.../net/minecraft/server/NetworkManager.java | 7 +
|
||||
.../minecraft/server/PlayerConnection.java | 54 +++++++
|
||||
4 files changed, 208 insertions(+)
|
||||
create mode 100644 src/main/java/com/destroystokyo/paper/network/PacketLimiter.java
|
||||
|
||||
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
||||
index 58e2a07079..4fcecf4095 100644
|
||||
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
||||
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
||||
@@ -392,6 +392,18 @@ public class PaperConfig {
|
||||
maxBookTotalSizeMultiplier = getDouble("settings.book-size.total-multiplier", maxBookTotalSizeMultiplier);
|
||||
}
|
||||
|
||||
+ public static int packetRateLimit = 250; // per second
|
||||
+ public static double packetRateLimitInterval = 10.0; // seconds
|
||||
+ public static String packetRateLimitKickMessage = "Sent too many packets";
|
||||
+ private static void packetRateLimit() {
|
||||
+ packetRateLimit = Math.max(-1, getInt("settings.spam-limiter.packets-per-second", packetRateLimit));
|
||||
+ packetRateLimitInterval = getDouble("settings.spam-limiter.packet-spam-interval", packetRateLimitInterval);
|
||||
+ if (packetRateLimitInterval <= 0.0 && packetRateLimit >= 0) {
|
||||
+ fatal("If packet rate limiting is enabled, the rate limit interval must be greater-than 0!");
|
||||
+ }
|
||||
+ packetRateLimitKickMessage = getString("messages.kick.packet-spam", packetRateLimitKickMessage);
|
||||
+ }
|
||||
+
|
||||
public static boolean asyncChunks = false;
|
||||
//public static boolean asyncChunkGeneration = true; // Leave out for now until we can control this
|
||||
//public static boolean asyncChunkGenThreadPerWorld = true; // Leave out for now until we can control this
|
||||
diff --git a/src/main/java/com/destroystokyo/paper/network/PacketLimiter.java b/src/main/java/com/destroystokyo/paper/network/PacketLimiter.java
|
||||
new file mode 100644
|
||||
index 0000000000..91c8c5f53a
|
||||
--- /dev/null
|
||||
+++ b/src/main/java/com/destroystokyo/paper/network/PacketLimiter.java
|
||||
@@ -0,0 +1,135 @@
|
||||
+package com.destroystokyo.paper.network;
|
||||
+
|
||||
+import java.util.Arrays;
|
||||
+
|
||||
+public class PacketLimiter {
|
||||
+
|
||||
+ /** multiplier to convert ns to ms */
|
||||
+ private static final double NANOSECONDS_TO_MILLISECONDS = 1.0e-6; // 1e3 / 1e9
|
||||
+
|
||||
+ /**
|
||||
+ * The time frame this packet limiter will count packets over (ms).
|
||||
+ */
|
||||
+ public final double intervalTime;
|
||||
+
|
||||
+ /**
|
||||
+ * The time each bucket will represent (ms).
|
||||
+ */
|
||||
+ public final double intervalResolution;
|
||||
+
|
||||
+ /**
|
||||
+ * Total buckets contained in this limiter.
|
||||
+ */
|
||||
+ public final int totalBuckets;
|
||||
+
|
||||
+ /**contains all packet data, note that indices of buckets will wrap around the array */
|
||||
+ private final int[] data;
|
||||
+
|
||||
+ /** pointer which represents the bucket containing the newest data */
|
||||
+ private int newestData;
|
||||
+
|
||||
+ /** the time attached to the bucket at newestData */
|
||||
+ private double lastBucketTime;
|
||||
+
|
||||
+ /** cached sum of all data */
|
||||
+ private int sum;
|
||||
+
|
||||
+ /**
|
||||
+ * Constructs a packetlimiter which will record total packets sent over the specified
|
||||
+ * interval time (in ms) with the specified number of buckets
|
||||
+ * @param intervalTime The specified interval time, in ms
|
||||
+ * @param totalBuckets The total number of buckets
|
||||
+ */
|
||||
+ public PacketLimiter(final double intervalTime, final int totalBuckets) {
|
||||
+ this.intervalTime = intervalTime;
|
||||
+ this.intervalResolution = intervalTime/(double)totalBuckets;
|
||||
+ this.totalBuckets = totalBuckets;
|
||||
+ this.data = new int[totalBuckets];
|
||||
+ }
|
||||
+
|
||||
+ /*
|
||||
+ * Stores number of packets according to their relative time. Each "bucket" will represent a time frame, according
|
||||
+ * to this.intervalResolution. The newestData pointer represents the bucket containing the newest piece of data.
|
||||
+ * The oldest piece of data is always the bucket after the newest data. When a new time frame is needed, the newest
|
||||
+ * data pointer is simply moved forward and previous data is either removed or overridden.
|
||||
+ *
|
||||
+ * For example, the following piece of code will sum all data older than the newest (starting from new -> older):
|
||||
+ *
|
||||
+ * int sum = 0;
|
||||
+ * for (int i = (newestData - 1) % buckets; i != newestData; i = (i - 1) % total_buckets) {
|
||||
+ * sum += buckets[i];
|
||||
+ * }
|
||||
+ *
|
||||
+ * Additionally, the sum of data is cached. Older data is automatically subtracted and newer data is automatically
|
||||
+ * summed. This makes calls made within a time interval of 'this.intervalResolution' O(1)
|
||||
+ *
|
||||
+ * Calls made over larger timers are O(n), with n ~ min(total_buckets, timePassed / bucket_interval)
|
||||
+ */
|
||||
+
|
||||
+ /**
|
||||
+ * Adds to this limiter's packet count. Old data is automatically purged, and the current time from {@link System#nanoTime()}
|
||||
+ * is used to record this data. Returns the new packet count.
|
||||
+ * @param packets The number of packets to attach to the current time
|
||||
+ * @return The new packet count
|
||||
+ */
|
||||
+ public int incrementPackets(final int packets) {
|
||||
+ return this.incrementPackets(System.nanoTime(), packets);
|
||||
+ }
|
||||
+
|
||||
+ private int incrementPackets(final long currentTime, final int packets) {
|
||||
+ final double timeMs = currentTime * NANOSECONDS_TO_MILLISECONDS;
|
||||
+ double timeDelta = timeMs - this.lastBucketTime;
|
||||
+
|
||||
+ if (timeDelta < 0.0) {
|
||||
+ // we presume the difference is small. nanotime always moves forward
|
||||
+ timeDelta = 0.0;
|
||||
+ }
|
||||
+
|
||||
+ if (timeDelta < this.intervalResolution) {
|
||||
+ this.data[this.newestData] += packets;
|
||||
+ return this.sum += packets;
|
||||
+ }
|
||||
+
|
||||
+ final int bucketsToMove = (int) (timeDelta / this.intervalResolution);
|
||||
+
|
||||
+ // With this expression there will be error, however it is small and will continue to the next counter.
|
||||
+ // In the end what matters is the DIFFERENCE in time between each bucket being the interval resolution,
|
||||
+ // which will remain approximately the case for this class' use case.
|
||||
+ // For large bucket counts (n > 1_000_000) we might have to consider the error.
|
||||
+ final double nextBucketTime = (this.lastBucketTime + bucketsToMove * this.intervalResolution);
|
||||
+
|
||||
+ if (bucketsToMove >= this.totalBuckets) {
|
||||
+ // we need to simply clear all data
|
||||
+ Arrays.fill(this.data, 0);
|
||||
+
|
||||
+ this.data[0] = packets;
|
||||
+ this.sum = packets;
|
||||
+ this.newestData = 0;
|
||||
+ this.lastBucketTime = timeMs;
|
||||
+
|
||||
+ return packets;
|
||||
+ }
|
||||
+
|
||||
+ // buckets we have no data for (since we've jumped ahead of them)
|
||||
+ for (int i = 1; i < bucketsToMove; ++i) {
|
||||
+ final int index = (this.newestData + i) % this.totalBuckets;
|
||||
+ this.sum -= this.data[index];
|
||||
+ this.data[index] = 0;
|
||||
+ }
|
||||
+
|
||||
+ final int newestDataIndex = (this.newestData + bucketsToMove) % this.totalBuckets;
|
||||
+ this.sum += packets - this.data[newestDataIndex]; // this.sum += packets; this.sum -= this.data[index]
|
||||
+ this.data[newestDataIndex] = packets;
|
||||
+ this.newestData = newestDataIndex;
|
||||
+ this.lastBucketTime = nextBucketTime;
|
||||
+
|
||||
+ return this.sum;
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Returns the total number of packets recorded in this interval.
|
||||
+ */
|
||||
+ public int getTotalPackets() {
|
||||
+ return this.sum;
|
||||
+ }
|
||||
+}
|
||||
diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java
|
||||
index 96a785af27..ff8b5e76f3 100644
|
||||
--- a/src/main/java/net/minecraft/server/NetworkManager.java
|
||||
+++ b/src/main/java/net/minecraft/server/NetworkManager.java
|
||||
@@ -150,6 +150,13 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
|
||||
}
|
||||
|
||||
private static <T extends PacketListener> void a(Packet<T> packet, PacketListener packetlistener) {
|
||||
+ // Paper start - Ratelimit packets
|
||||
+ if (packetlistener instanceof PlayerConnection) {
|
||||
+ if (((PlayerConnection)packetlistener).rateLimitPacket(packet)) {
|
||||
+ return; // we've been killed as a result of rate limiting
|
||||
+ }
|
||||
+ }
|
||||
+ // Paper end
|
||||
packet.a((T) packetlistener); // CraftBukkit - decompile error
|
||||
}
|
||||
|
||||
diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java
|
||||
index 3869cdeeb6..226a871aaf 100644
|
||||
--- a/src/main/java/net/minecraft/server/PlayerConnection.java
|
||||
+++ b/src/main/java/net/minecraft/server/PlayerConnection.java
|
||||
@@ -140,6 +140,60 @@ public class PlayerConnection implements PacketListenerPlayIn {
|
||||
return (this.player == null) ? null : (CraftPlayer) this.player.getBukkitEntity();
|
||||
}
|
||||
// CraftBukkit end
|
||||
+ // Paper start - ratelimit packets
|
||||
+ /*
|
||||
+ * We cannot rely on tick() being called. If a server lags we could end up kicking everyone!
|
||||
+ * So we must rely on our own timer.
|
||||
+ */
|
||||
+ private final com.destroystokyo.paper.network.PacketLimiter packetLimiter = com.destroystokyo.paper.PaperConfig.packetRateLimit < 0 ?
|
||||
+ null : new com.destroystokyo.paper.network.PacketLimiter(com.destroystokyo.paper.PaperConfig.packetRateLimitInterval * 1000.0, 100);
|
||||
+ private boolean kickedForPacketSpam;
|
||||
+
|
||||
+ private static final java.text.DecimalFormat ONE_DECIMAL_PLACE = new java.text.DecimalFormat("0.0");
|
||||
+
|
||||
+ /** @return {@code true} if the client has been killed as a result of rate limiting, {@code false} if not */
|
||||
+ boolean rateLimitPacket(final Packet<?> packet) {
|
||||
+ final com.destroystokyo.paper.network.PacketLimiter limiter = this.packetLimiter;
|
||||
+ if (limiter == null) {
|
||||
+ // not configured
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ // avoid contending the lock if we've been kicked already
|
||||
+ if (this.kickedForPacketSpam) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ final int limit = com.destroystokyo.paper.PaperConfig.packetRateLimit;
|
||||
+
|
||||
+ synchronized (limiter) {
|
||||
+ // we need to re-check this, as it could have changed during the lock hold
|
||||
+ if (this.kickedForPacketSpam) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ final int packets = limiter.incrementPackets(1);
|
||||
+
|
||||
+ if (packets / (limiter.intervalTime / 1000.0) <= limit) {
|
||||
+ // below limit
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ this.kickedForPacketSpam = true;
|
||||
+ this.minecraftServer.postToMainThread(() -> {
|
||||
+ if (PlayerConnection.this.processedDisconnect) {
|
||||
+ return; // no point, we've disconnected already
|
||||
+ }
|
||||
+ PlayerConnection.this.disconnect(com.destroystokyo.paper.PaperConfig.packetRateLimitKickMessage);
|
||||
+ PlayerConnection.LOGGER.warn("{} was kicked for sending too many packets! {} in the last {} seconds",
|
||||
+ PlayerConnection.this.player.getDisplayName().getString(), packets,
|
||||
+ ONE_DECIMAL_PLACE.format(limiter.intervalTime / 1000.0));
|
||||
+ });
|
||||
+
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ // Paper end
|
||||
|
||||
public void tick() {
|
||||
this.syncPosition();
|
||||
--
|
||||
2.20.1
|
||||
|
||||
Reference in New Issue
Block a user