Files
Purpur/patches/server/0040-Rate-limit-incoming-packets-from-players.patch
William Blake Galbreath a32448bda1 Merge in Tuinity patches
2020-03-08 12:49:59 -05:00

278 lines
11 KiB
Diff

From 1fa6081a9f5c7bf0fc63a0530d266b7783e53727 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <Blake.Galbreath@GMail.com>
Date: Fri, 7 Feb 2020 09:45:09 -0600
Subject: [PATCH] Rate limit incoming packets from players
---
.../net/minecraft/server/NetworkManager.java | 7 +
.../minecraft/server/PlayerConnection.java | 56 +++++++
.../java/net/pl3x/purpur/PurpurConfig.java | 12 ++
.../pl3x/purpur/network/PacketLimiter.java | 147 ++++++++++++++++++
4 files changed, 222 insertions(+)
create mode 100644 src/main/java/net/pl3x/purpur/network/PacketLimiter.java
diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java
index 211a6d720..a1304240d 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) {
+ // Purpur start - Ratelimit packets
+ if (packetlistener instanceof PlayerConnection) {
+ if (((PlayerConnection)packetlistener).rateLimitPacket(packet)) {
+ return; // we've been killed as a result of rate limiting
+ }
+ }
+ // Purpur 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 895e34ed3..67afa457e 100644
--- a/src/main/java/net/minecraft/server/PlayerConnection.java
+++ b/src/main/java/net/minecraft/server/PlayerConnection.java
@@ -140,6 +140,62 @@ public class PlayerConnection implements PacketListenerPlayIn {
return (this.player == null) ? null : (CraftPlayer) this.player.getBukkitEntity();
}
// CraftBukkit end
+ // Purpur 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 net.pl3x.purpur.network.PacketLimiter packetLimiter = net.pl3x.purpur.PurpurConfig.packetRateLimit < 0 ?
+ null : new net.pl3x.purpur.network.PacketLimiter(net.pl3x.purpur.PurpurConfig.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 net.pl3x.purpur.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 = net.pl3x.purpur.PurpurConfig.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(net.pl3x.purpur.PurpurConfig.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;
+ }
+ }
+ // Purpur end
public void tick() {
this.syncPosition();
diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java
index 3670b4d48..e1a1ef860 100644
--- a/src/main/java/net/pl3x/purpur/PurpurConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java
@@ -142,6 +142,18 @@ public class PurpurConfig {
loggerSuppressWorldGenFeatureDeserializationError = getBoolean("settings.logger.suppress-world-gen-feature-deserialization-errors", loggerSuppressWorldGenFeatureDeserializationError);
}
+ 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 packetLimiterSettings() {
+ packetRateLimit = Math.max(-1, getInt("settings.packet-limiter.packets-per-second", packetRateLimit));
+ packetRateLimitInterval = getDouble("settings.packet-limiter.packet-spam-interval", packetRateLimitInterval);
+ if (packetRateLimitInterval <= 0.0 && packetRateLimit >= 0) {
+ log(Level.SEVERE, "If packet rate limiting is enabled, the rate limit interval must be greater than 0!");
+ }
+ packetRateLimitKickMessage = getString("settings.packet-limiter.kick-message", packetRateLimitKickMessage);
+ }
+
public static boolean dontSendUselessEntityPackets = false;
public static boolean fixItemPositionDesync = false;
private static void dontSendUselessEntityPackets() {
diff --git a/src/main/java/net/pl3x/purpur/network/PacketLimiter.java b/src/main/java/net/pl3x/purpur/network/PacketLimiter.java
new file mode 100644
index 000000000..e51e7eb9d
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/network/PacketLimiter.java
@@ -0,0 +1,147 @@
+package net.pl3x.purpur.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;
+ }
+}
--
2.24.0