mirror of
https://github.com/PaperMC/Velocity.git
synced 2026-02-17 14:37:43 +01:00
Add basic packet limiter
This commit is contained in:
@@ -93,6 +93,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
private @Nullable Favicon favicon;
|
||||
@Expose
|
||||
private boolean forceKeyAuthentication = true; // Added in 1.19
|
||||
@Expose
|
||||
private PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.DEFAULT;
|
||||
|
||||
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
|
||||
Query query, Metrics metrics) {
|
||||
@@ -109,7 +111,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
|
||||
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
|
||||
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
|
||||
boolean forceKeyAuthentication) {
|
||||
boolean forceKeyAuthentication, PacketLimiterConfig packetLimiterConfig) {
|
||||
this.bind = bind;
|
||||
this.motd = motd;
|
||||
this.showMaxPlayers = showMaxPlayers;
|
||||
@@ -128,6 +130,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
this.query = query;
|
||||
this.metrics = metrics;
|
||||
this.forceKeyAuthentication = forceKeyAuthentication;
|
||||
this.packetLimiterConfig = packetLimiterConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,6 +452,10 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
return advanced.isEnableReusePort();
|
||||
}
|
||||
|
||||
public PacketLimiterConfig getPacketLimiterConfig() {
|
||||
return packetLimiterConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
@@ -466,6 +473,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
.add("favicon", favicon)
|
||||
.add("enablePlayerAddressLogging", enablePlayerAddressLogging)
|
||||
.add("forceKeyAuthentication", forceKeyAuthentication)
|
||||
.add("packetLimiterConfig", packetLimiterConfig)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -557,6 +565,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
final boolean kickExisting = config.getOrElse("kick-existing-players", false);
|
||||
final boolean enablePlayerAddressLogging = config.getOrElse(
|
||||
"enable-player-address-logging", true);
|
||||
final PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.fromConfig(config.get("packet-limiter"));
|
||||
|
||||
// Throw an exception if the forwarding-secret file is empty and the proxy is using a
|
||||
// forwarding mode that requires it.
|
||||
@@ -584,7 +593,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
new Advanced(advancedConfig),
|
||||
new Query(queryConfig),
|
||||
new Metrics(metricsConfig),
|
||||
forceKeyAuthentication
|
||||
forceKeyAuthentication,
|
||||
packetLimiterConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -987,4 +997,27 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for packet limiting.
|
||||
*
|
||||
* @param interval the interval in seconds to measure packets over
|
||||
* @param pps the maximum number of packets per second allowed
|
||||
* @param bytes the maximum number of bytes per second allowed
|
||||
*/
|
||||
public record PacketLimiterConfig(int interval, int pps, int bytes) {
|
||||
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1);
|
||||
|
||||
public static PacketLimiterConfig fromConfig(CommentedConfig config) {
|
||||
if (config != null) {
|
||||
return new PacketLimiterConfig(
|
||||
config.getIntOrElse("interval", DEFAULT.interval()),
|
||||
config.getIntOrElse("pps", DEFAULT.pps()),
|
||||
config.getIntOrElse("bytes", DEFAULT.bytes())
|
||||
);
|
||||
} else {
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
||||
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
|
||||
@@ -72,6 +74,17 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
|
||||
new HandshakeSessionHandler(connection, this.server));
|
||||
ch.pipeline().addLast(Connections.HANDLER, connection);
|
||||
|
||||
VelocityConfiguration.PacketLimiterConfig packetLimiterConfig =
|
||||
server.getConfiguration().getPacketLimiterConfig();
|
||||
int configuredInterval = packetLimiterConfig.interval();
|
||||
int configuredPPS = packetLimiterConfig.pps();
|
||||
int configuredBytes = packetLimiterConfig.bytes();
|
||||
|
||||
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPPS > 0)) {
|
||||
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
|
||||
new SimpleBytesPerSecondLimiter(configuredPPS, configuredBytes, configuredInterval)
|
||||
);
|
||||
}
|
||||
if (this.server.getConfiguration().isProxyProtocol()) {
|
||||
ch.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.network.limiter;
|
||||
|
||||
/**
|
||||
* PacketLimiter enforces a limit on the number of bytes processed over a time window.
|
||||
* Implementations should be thread-safe.
|
||||
*/
|
||||
public interface PacketLimiter {
|
||||
/**
|
||||
* Attempts to record the specified number of bytes within the current window.
|
||||
*
|
||||
* @param bytes the number of bytes to record
|
||||
* @return true if the bytes are allowed and recorded; false if the limit would be exceeded
|
||||
*/
|
||||
boolean account(int bytes);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.network.limiter;
|
||||
|
||||
import com.velocitypowered.proxy.util.IntervalledCounter;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A moving-window limiter over a configurable number of seconds.
|
||||
* It enforces both packets-per-second and average bytes-per-second limits.
|
||||
* The effective cap over the full window equals limitPerSecond * windowSeconds.
|
||||
*/
|
||||
public final class SimpleBytesPerSecondLimiter implements PacketLimiter {
|
||||
@Nullable
|
||||
private final IntervalledCounter bytesCounter;
|
||||
@Nullable
|
||||
private final IntervalledCounter packetsCounter;
|
||||
private final int packetsPerSecond;
|
||||
private final int bytesPerSecond;
|
||||
|
||||
/**
|
||||
* Creates a new SimpleBytesPerSecondLimiter.
|
||||
*
|
||||
* @param packetsPerSecond maximum average packets per second allowed (> 0)
|
||||
* @param bytesPerSecond maximum average bytes per second allowed (> 0)
|
||||
* @param windowSeconds number of seconds in the moving window (> 0)
|
||||
*/
|
||||
public SimpleBytesPerSecondLimiter(int packetsPerSecond, int bytesPerSecond, int windowSeconds) {
|
||||
this.packetsPerSecond = packetsPerSecond;
|
||||
if (windowSeconds <= 0) {
|
||||
throw new IllegalArgumentException("windowSeconds must be > 0");
|
||||
}
|
||||
this.bytesPerSecond = bytesPerSecond;
|
||||
this.packetsCounter = packetsPerSecond > 0 ? new IntervalledCounter(windowSeconds) : null;
|
||||
this.bytesCounter = bytesPerSecond > 0 ? new IntervalledCounter(windowSeconds) : null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the given payload length as one packet and returns whether it is allowed.
|
||||
*/
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
@Override
|
||||
public boolean account(int bytes) {
|
||||
long currTime = System.nanoTime();
|
||||
if (packetsCounter != null) {
|
||||
packetsCounter.updateAndAdd(1, currTime);
|
||||
if (packetsCounter.getRate() > packetsPerSecond) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (bytesCounter != null) {
|
||||
bytesCounter.updateAndAdd(bytes, currTime);
|
||||
if (bytesCounter.getRate() > bytesPerSecond) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package com.velocitypowered.proxy.protocol.netty;
|
||||
import static io.netty.util.ByteProcessor.FIND_NON_NUL;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
@@ -32,6 +33,7 @@ import io.netty.handler.codec.CorruptedFrameException;
|
||||
import java.util.List;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding.
|
||||
@@ -52,6 +54,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
private final ProtocolUtils.Direction direction;
|
||||
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
|
||||
private StateRegistry state;
|
||||
@Nullable
|
||||
private PacketLimiter packetLimiter;
|
||||
|
||||
/**
|
||||
* Creates a new {@code MinecraftVarintFrameDecoder} decoding packets from the specified {@code Direction}.
|
||||
@@ -131,6 +135,15 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
// note that zero-length packets are ignored
|
||||
if (length > 0) {
|
||||
// If enabled, rate-limit serverbound payload bytes based on frame length
|
||||
if (packetLimiter != null) {
|
||||
if (!packetLimiter.account(length)) {
|
||||
throw new QuietDecoderException(
|
||||
"Rate limit exceeded while processing packets for %s".formatted(
|
||||
ctx.channel().remoteAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
} else {
|
||||
@@ -240,4 +253,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
public void setState(StateRegistry stateRegistry) {
|
||||
this.state = stateRegistry;
|
||||
}
|
||||
|
||||
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
|
||||
this.packetLimiter = packetLimiter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.util;
|
||||
|
||||
/**
|
||||
* IntervalledCounter maintains a rolling sum of values associated with timestamps, keeping
|
||||
* only those entries that fall within a fixed time interval from the most recent timestamp.
|
||||
*
|
||||
* <p>Time values must be provided in the same unit as {@link System#nanoTime()} (nanoseconds),
|
||||
* and the configured interval is also expressed in nanoseconds. Callers are expected to
|
||||
* periodically advance the counter to the current time using {@link #updateCurrentTime()} or
|
||||
* {@link #updateCurrentTime(long)} to evict expired entries before adding new ones via
|
||||
* {@link #addTime(long)} or {@link #addTime(long, long)}.</p>
|
||||
*
|
||||
* <p>This class is not thread-safe. If multiple threads access an instance concurrently,
|
||||
* external synchronization is required.</p>
|
||||
*/
|
||||
public final class IntervalledCounter {
|
||||
|
||||
private static final int INITIAL_SIZE = 8;
|
||||
|
||||
/**
|
||||
* Ring buffer holding the timestamp (in nanoseconds) for each data point.
|
||||
*/
|
||||
protected long[] times;
|
||||
/**
|
||||
* Ring buffer holding the count associated with each timestamp.
|
||||
*/
|
||||
protected long[] counts;
|
||||
/**
|
||||
* The sliding window size in nanoseconds. Only entries with time >= (currentTime - interval)
|
||||
* are considered part of the window.
|
||||
*/
|
||||
protected final long interval;
|
||||
/**
|
||||
* Cached lower bound of the window (in nanoseconds) after the last update.
|
||||
*/
|
||||
protected long minTime;
|
||||
/**
|
||||
* Running sum of all counts currently within the window.
|
||||
*/
|
||||
protected long sum;
|
||||
/**
|
||||
* Head index (inclusive) of the ring buffer.
|
||||
*/
|
||||
protected int head; // inclusive
|
||||
/**
|
||||
* Tail index (exclusive) of the ring buffer.
|
||||
*/
|
||||
protected int tail; // exclusive
|
||||
|
||||
/**
|
||||
* Creates a new counter with the specified interval.
|
||||
*
|
||||
* @param interval the window size in nanoseconds (compatible with {@link System#nanoTime()})
|
||||
*/
|
||||
public IntervalledCounter(final long interval) {
|
||||
this.times = new long[INITIAL_SIZE];
|
||||
this.counts = new long[INITIAL_SIZE];
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the window to the current time using {@link System#nanoTime()}, evicting any
|
||||
* data points that have fallen outside of the interval and updating the running sum.
|
||||
*/
|
||||
public void updateCurrentTime() {
|
||||
this.updateCurrentTime(System.nanoTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the window to the provided time, evicting any data points older than
|
||||
* {@code currentTime - interval} and updating the running sum.
|
||||
*
|
||||
* @param currentTime the current time in nanoseconds (as from {@link System#nanoTime()})
|
||||
*/
|
||||
public void updateCurrentTime(final long currentTime) {
|
||||
long sum = this.sum;
|
||||
int head = this.head;
|
||||
final int tail = this.tail;
|
||||
final long minTime = currentTime - this.interval;
|
||||
|
||||
final int arrayLen = this.times.length;
|
||||
|
||||
// guard against overflow by using subtraction
|
||||
while (head != tail && this.times[head] - minTime < 0) {
|
||||
sum -= this.counts[head];
|
||||
// there are two ways we can do this:
|
||||
// 1. free the count when adding
|
||||
// 2. free it now
|
||||
// option #2
|
||||
this.counts[head] = 0;
|
||||
if (++head >= arrayLen) {
|
||||
head = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.sum = sum;
|
||||
this.head = head;
|
||||
this.minTime = minTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single unit at the specified timestamp, assuming the timestamp is within the current
|
||||
* window. If the timestamp is older than the current window lower bound, the value is ignored.
|
||||
* This method does not automatically advance the window; callers should invoke
|
||||
* {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand.
|
||||
*
|
||||
* @param currTime the timestamp in nanoseconds
|
||||
*/
|
||||
public void addTime(final long currTime) {
|
||||
this.addTime(currTime, 1L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@code count} units at the specified timestamp, assuming the timestamp is within the
|
||||
* current window. If the timestamp is older than {@code minTime}, the value is ignored.
|
||||
* This method does not automatically advance the window; callers should invoke
|
||||
* {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand.
|
||||
*
|
||||
* @param currTime the timestamp in nanoseconds
|
||||
* @param count the amount to add (non-negative)
|
||||
*/
|
||||
public void addTime(final long currTime, final long count) {
|
||||
// guard against overflow by using subtraction
|
||||
if (currTime - this.minTime < 0) {
|
||||
return;
|
||||
}
|
||||
int nextTail = (this.tail + 1) % this.times.length;
|
||||
if (nextTail == this.head) {
|
||||
this.resize();
|
||||
nextTail = (this.tail + 1) % this.times.length;
|
||||
}
|
||||
|
||||
this.times[this.tail] = currTime;
|
||||
this.counts[this.tail] += count;
|
||||
this.sum += count;
|
||||
this.tail = nextTail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that advances the window to the current time and then adds {@code count}
|
||||
* units at that time.
|
||||
*
|
||||
* @param count the amount to add (non-negative)
|
||||
*/
|
||||
public void updateAndAdd(final long count) {
|
||||
final long currTime = System.nanoTime();
|
||||
this.updateCurrentTime(currTime);
|
||||
this.addTime(currTime, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that advances the window to {@code currTime} and then adds {@code count}
|
||||
* units at that time.
|
||||
*
|
||||
* @param count the amount to add (non-negative)
|
||||
* @param currTime the timestamp in nanoseconds
|
||||
*/
|
||||
public void updateAndAdd(final long count, final long currTime) {
|
||||
this.updateCurrentTime(currTime);
|
||||
this.addTime(currTime, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Doubles the capacity of the internal ring buffers, preserving the order of existing data.
|
||||
*/
|
||||
private void resize() {
|
||||
final long[] oldElements = this.times;
|
||||
final long[] oldCounts = this.counts;
|
||||
final long[] newElements = new long[this.times.length * 2];
|
||||
final long[] newCounts = new long[this.times.length * 2];
|
||||
this.times = newElements;
|
||||
this.counts = newCounts;
|
||||
|
||||
final int head = this.head;
|
||||
final int tail = this.tail;
|
||||
final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
|
||||
this.head = 0;
|
||||
this.tail = size;
|
||||
|
||||
if (tail >= head) {
|
||||
// sequentially ordered from [head, tail)
|
||||
System.arraycopy(oldElements, head, newElements, 0, size);
|
||||
System.arraycopy(oldCounts, head, newCounts, 0, size);
|
||||
} else {
|
||||
// ordered from [head, length)
|
||||
// then followed by [0, tail)
|
||||
|
||||
System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
|
||||
System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
|
||||
|
||||
System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head);
|
||||
System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current rate in units per second based on the rolling sum and the configured
|
||||
* interval. Specifically: {@code sum / (intervalSeconds)} where {@code intervalSeconds}
|
||||
* equals {@code interval / 1e9}.
|
||||
*
|
||||
* @return the rate in units per second for the current window
|
||||
*/
|
||||
public double getRate() {
|
||||
return (double)this.sum / ((double)this.interval * 1.0E-9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured interval size in nanoseconds.
|
||||
*
|
||||
* @return the interval size in nanoseconds
|
||||
*/
|
||||
public long getInterval() {
|
||||
return this.interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rolling sum of all counts currently within the window.
|
||||
*
|
||||
* @return the rolling sum
|
||||
*/
|
||||
public long getSum() {
|
||||
return this.sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of data points currently stored in the internal ring buffer. This may be
|
||||
* less than or equal to the number of points added since older entries may have been evicted.
|
||||
*
|
||||
* @return the number of stored data points
|
||||
*/
|
||||
public int totalDataPoints() {
|
||||
return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head));
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,11 @@ sample-players-in-ping = false
|
||||
# If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs
|
||||
enable-player-address-logging = true
|
||||
|
||||
[packet-limiter]
|
||||
interval = 3000
|
||||
pps = 100
|
||||
bytes = 1000
|
||||
|
||||
[servers]
|
||||
# Configure your servers here. Each key represents the server's name, and the value
|
||||
# represents the IP address of the server to connect to.
|
||||
|
||||
Reference in New Issue
Block a user