Rate limit packets incoming from players

This commit is contained in:
William Blake Galbreath
2019-07-27 16:37:07 -05:00
parent 42ca953aa3
commit c50b5e52e3

View File

@@ -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