diff --git a/src/main/java/ua/nanit/limbo/configuration/LimboConfig.java b/src/main/java/ua/nanit/limbo/configuration/LimboConfig.java index 615b9e2..11cceb5 100644 --- a/src/main/java/ua/nanit/limbo/configuration/LimboConfig.java +++ b/src/main/java/ua/nanit/limbo/configuration/LimboConfig.java @@ -73,8 +73,8 @@ public final class LimboConfig { private boolean useTrafficLimits; private int maxPacketSize; - private int maxPacketsPerSec; - private int maxBytesPerSec; + private double interval; + private double maxPacketRate; public LimboConfig(Path root) { this.root = root; @@ -134,9 +134,9 @@ public final class LimboConfig { workerGroupSize = conf.node("netty", "threads", "workerGroup").getInt(4); useTrafficLimits = conf.node("traffic", "enable").getBoolean(false); - maxPacketSize = conf.node("traffic", "packetSize").getInt(-1); - maxPacketsPerSec = conf.node("traffic", "packets").getInt(-1); - maxBytesPerSec = conf.node("traffic", "bytes").getInt(-1); + maxPacketSize = conf.node("traffic", "maxPacketSize").getInt(-1); + interval = conf.node("traffic", "interval").getDouble(-1.0); + maxPacketRate = conf.node("traffic", "maxPacketRate").getDouble(-1.0); } private BufferedReader getReader() throws IOException { @@ -269,11 +269,11 @@ public final class LimboConfig { return maxPacketSize; } - public int getMaxPacketsPerSec() { - return maxPacketsPerSec; + public double getInterval() { + return interval; } - public int getMaxBytesPerSec() { - return maxBytesPerSec; + public double getMaxPacketRate() { + return maxPacketRate; } } diff --git a/src/main/java/ua/nanit/limbo/connection/ClientChannelInitializer.java b/src/main/java/ua/nanit/limbo/connection/ClientChannelInitializer.java index 6f7fc94..57d7944 100644 --- a/src/main/java/ua/nanit/limbo/connection/ClientChannelInitializer.java +++ b/src/main/java/ua/nanit/limbo/connection/ClientChannelInitializer.java @@ -50,8 +50,8 @@ public class ClientChannelInitializer extends ChannelInitializer { if (server.getConfig().isUseTrafficLimits()) { pipeline.addLast("traffic_limit", new ChannelTrafficHandler( server.getConfig().getMaxPacketSize(), - server.getConfig().getMaxPacketsPerSec(), - server.getConfig().getMaxBytesPerSec() + server.getConfig().getInterval(), + server.getConfig().getMaxPacketRate() )); } diff --git a/src/main/java/ua/nanit/limbo/connection/pipeline/ChannelTrafficHandler.java b/src/main/java/ua/nanit/limbo/connection/pipeline/ChannelTrafficHandler.java index b9acaf1..7d1babc 100644 --- a/src/main/java/ua/nanit/limbo/connection/pipeline/ChannelTrafficHandler.java +++ b/src/main/java/ua/nanit/limbo/connection/pipeline/ChannelTrafficHandler.java @@ -6,21 +6,18 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import org.jetbrains.annotations.NotNull; import ua.nanit.limbo.server.Logger; +import java.util.Arrays; + public class ChannelTrafficHandler extends ChannelInboundHandlerAdapter { - private final int packetSize; - private final int packetsPerSec; - private final int bytesPerSec; + private final int maxPacketSize; + private final double maxPacketRate; + private final PacketBucket packetBucket; - private int packetsCounter; - private int bytesCounter; - - private long lastPacket; - - public ChannelTrafficHandler(int packetSize, int packetsPerSec, int bytesPerSec) { - this.packetSize = packetSize; - this.packetsPerSec = packetsPerSec; - this.bytesPerSec = bytesPerSec; + public ChannelTrafficHandler(int maxPacketSize, double interval, double maxPacketRate) { + this.maxPacketSize = maxPacketSize; + this.maxPacketRate = maxPacketRate; + this.packetBucket = (interval > 0.0 && maxPacketRate > 0.0) ? new PacketBucket(interval * 1000.0, 150) : null; } @Override @@ -29,47 +26,86 @@ public class ChannelTrafficHandler extends ChannelInboundHandlerAdapter { ByteBuf in = (ByteBuf) msg; int bytes = in.readableBytes(); - if (packetSize > 0 && bytes > packetSize) { - closeConnection(ctx, "Closed %s due too large packet size (%d bytes)", ctx.channel().remoteAddress(), bytes); + if (maxPacketSize > 0 && bytes > maxPacketSize) { + closeConnection(ctx, "Closed %s due to large packet size (%d bytes)", ctx.channel().remoteAddress(), bytes); return; } - if (!measureTraffic(ctx, bytes)) return; + if (packetBucket != null) { + packetBucket.incrementPackets(1); + if (packetBucket.getCurrentPacketRate() > maxPacketRate) { + closeConnection(ctx, "Closed %s due to many packets sent (%d in the last %.1f seconds)", ctx.channel().remoteAddress(), packetBucket.sum, (packetBucket.intervalTime / 1000.0)); + return; + } + } } super.channelRead(ctx, msg); } - private boolean measureTraffic(ChannelHandlerContext ctx, int bytes) { - if (packetsPerSec < 0 && bytesPerSec < 0) return true; - - long time = System.currentTimeMillis(); - - if (time - lastPacket >= 1000) { - bytesCounter = 0; - packetsCounter = 0; - } - - packetsCounter++; - bytesCounter += bytes; - - if (packetsPerSec > 0 && packetsCounter > packetsPerSec) { - closeConnection(ctx, "Closed %s due too frequent packet sending (%d per sec)", ctx.channel().remoteAddress(), packetsCounter); - return false; - } - - if (bytesPerSec > 0 && bytesCounter > bytesPerSec) { - closeConnection(ctx, "Closed %s due too many bytes sent per second (%d per sec)", ctx.channel().remoteAddress(), bytesCounter); - return false; - } - - lastPacket = time; - - return true; - } - private void closeConnection(ChannelHandlerContext ctx, String reason, Object... args) { ctx.close(); Logger.info(reason, args); } + + private static class PacketBucket { + private static final double NANOSECONDS_TO_MILLISECONDS = 1.0e-6; + private static final int MILLISECONDS_TO_SECONDS = 1000; + + private final double intervalTime; + private final double intervalResolution; + private final int[] data; + private int newestData; + private double lastBucketTime; + private int sum; + + public PacketBucket(final double intervalTime, final int totalBuckets) { + this.intervalTime = intervalTime; + this.intervalResolution = intervalTime / totalBuckets; + this.data = new int[totalBuckets]; + } + + public void incrementPackets(final int packets) { + double timeMs = System.nanoTime() * NANOSECONDS_TO_MILLISECONDS; + double timeDelta = timeMs - this.lastBucketTime; + + if (timeDelta < 0.0) { + timeDelta = 0.0; + } + + if (timeDelta < this.intervalResolution) { + this.data[this.newestData] += packets; + this.sum += packets; + return; + } + + int bucketsToMove = (int)(timeDelta / this.intervalResolution); + double nextBucketTime = this.lastBucketTime + bucketsToMove * this.intervalResolution; + + if (bucketsToMove >= this.data.length) { + Arrays.fill(this.data, 0); + this.data[0] = packets; + this.sum = packets; + this.newestData = 0; + this.lastBucketTime = timeMs; + return; + } + + for (int i = 1; i < bucketsToMove; ++i) { + int index = (this.newestData + i) % this.data.length; + this.sum -= this.data[index]; + this.data[index] = 0; + } + + int newestDataIndex = (this.newestData + bucketsToMove) % this.data.length; + this.sum += packets - this.data[newestDataIndex]; + this.data[newestDataIndex] = packets; + this.newestData = newestDataIndex; + this.lastBucketTime = nextBucketTime; + } + + public double getCurrentPacketRate() { + return this.sum / (this.intervalTime / MILLISECONDS_TO_SECONDS); + } + } } diff --git a/src/main/resources/settings.yml b/src/main/resources/settings.yml index 62aaceb..2b58479 100644 --- a/src/main/resources/settings.yml +++ b/src/main/resources/settings.yml @@ -114,18 +114,20 @@ netty: bossGroup: 1 workerGroup: 4 -# [Experimental] # Options to check incoming traffic and kick potentially malicious connections. # Take into account that player can send many small packets, for example just moving mouse. traffic: # If true, then additional handler will be added to channel pipeline - enable: false + enable: true # Max packet size in bytes # Unlimited if -1 - packetSize: 1024 - # How many packets per second allowed for single connection - # Ignored if -1 - packets: -1 - # How many bytes per second allowed for single connection - # Ignored if -1 - bytes: 2048 \ No newline at end of file + maxPacketSize: 4096 + # The interval to measure packets over + # Lowering this value will limit peak packets from players which would target people with bad connections + # Raising this value will allow higher peak packet rates, which will help with people who have poor connections + # Ignored if -1.0 + interval: 7.0 + # The maximum maximum packets per second for players + # It is measured over the configured interval + # Ignored if -1.0 + maxPacketRate: 500.0