mirror of
https://github.com/PurpurMC/Purpur.git
synced 2026-02-18 17:07:43 +01:00
278 lines
11 KiB
Diff
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
|
|
|