geforkt von Mirrors/Velocity
Add connection attempt rate-limiting.
Dieser Commit ist enthalten in:
Ursprung
db8b7c807c
Commit
254508a5cf
@ -21,6 +21,7 @@ import com.velocitypowered.proxy.command.CommandManager;
|
|||||||
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
|
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
|
||||||
import com.velocitypowered.proxy.util.AddressUtil;
|
import com.velocitypowered.proxy.util.AddressUtil;
|
||||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||||
|
import com.velocitypowered.proxy.util.Ratelimiter;
|
||||||
import com.velocitypowered.proxy.util.ServerMap;
|
import com.velocitypowered.proxy.util.ServerMap;
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
@ -71,6 +72,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private final Ratelimiter ipAttemptLimiter = new Ratelimiter(3000); // TODO: Configurable.
|
||||||
|
|
||||||
private VelocityServer() {
|
private VelocityServer() {
|
||||||
commandManager.registerCommand("velocity", new VelocityCommand());
|
commandManager.registerCommand("velocity", new VelocityCommand());
|
||||||
@ -162,6 +164,10 @@ public class VelocityServer implements ProxyServer {
|
|||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Ratelimiter getIpAttemptLimiter() {
|
||||||
|
return ipAttemptLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean registerConnection(ConnectedPlayer connection) {
|
public boolean registerConnection(ConnectedPlayer connection) {
|
||||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||||
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||||
|
@ -15,6 +15,7 @@ import net.kyori.text.TextComponent;
|
|||||||
import net.kyori.text.TranslatableComponent;
|
import net.kyori.text.TranslatableComponent;
|
||||||
import net.kyori.text.format.TextColor;
|
import net.kyori.text.format.TextColor;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||||
@ -50,6 +51,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
|||||||
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
|
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
InetAddress address = ((InetSocketAddress) connection.getChannel().remoteAddress()).getAddress();
|
||||||
|
if (!VelocityServer.getServer().getIpAttemptLimiter().attempt(address)) {
|
||||||
|
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
connection.setSessionHandler(new LoginSessionHandler(connection));
|
connection.setSessionHandler(new LoginSessionHandler(connection));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -91,6 +91,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
VelocityServer.getServer().getHttpClient()
|
VelocityServer.getServer().getHttpClient()
|
||||||
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
|
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
|
||||||
.thenAcceptAsync(profileResponse -> {
|
.thenAcceptAsync(profileResponse -> {
|
||||||
|
if (inbound.isClosed()) {
|
||||||
|
// The player disconnected after we authenticated them.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
inbound.enableEncryption(decryptedSharedSecret);
|
inbound.enableEncryption(decryptedSharedSecret);
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
|
41
proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java
Normale Datei
41
proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java
Normale Datei
@ -0,0 +1,41 @@
|
|||||||
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Ticker;
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class Ratelimiter {
|
||||||
|
private final Cache<InetAddress, Long> expiringCache;
|
||||||
|
private final long timeoutNanos;
|
||||||
|
|
||||||
|
public Ratelimiter(long timeoutMs) {
|
||||||
|
this(timeoutMs, Ticker.systemTicker());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Ratelimiter(long timeoutMs, Ticker ticker) {
|
||||||
|
this.timeoutNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMs);
|
||||||
|
this.expiringCache = CacheBuilder.newBuilder()
|
||||||
|
.ticker(ticker)
|
||||||
|
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
|
||||||
|
.expireAfterWrite(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean attempt(InetAddress address) {
|
||||||
|
long expectedNewValue = System.nanoTime() + timeoutNanos;
|
||||||
|
long last;
|
||||||
|
try {
|
||||||
|
last = expiringCache.get(address, () -> expectedNewValue);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
// It should be impossible for this to fail.
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
return expectedNewValue == last;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
|
import com.google.common.base.Ticker;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class RatelimiterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void attempt() {
|
||||||
|
long base = System.nanoTime();
|
||||||
|
AtomicLong extra = new AtomicLong();
|
||||||
|
Ticker testTicker = new Ticker() {
|
||||||
|
@Override
|
||||||
|
public long read() {
|
||||||
|
return base + extra.get();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ratelimiter ratelimiter = new Ratelimiter(1000, testTicker);
|
||||||
|
assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
|
||||||
|
assertFalse(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
|
||||||
|
extra.addAndGet(TimeUnit.SECONDS.toNanos(2));
|
||||||
|
assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.server.ServerInfo;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ServerMapTest {
|
||||||
|
private static final InetSocketAddress TEST_ADDRESS = new InetSocketAddress(InetAddress.getLoopbackAddress(), 25565);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void respectsCaseInsensitivity() {
|
||||||
|
ServerMap map = new ServerMap();
|
||||||
|
ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS);
|
||||||
|
map.register(info);
|
||||||
|
|
||||||
|
assertEquals(Optional.of(info), map.getServer("TestServer"));
|
||||||
|
assertEquals(Optional.of(info), map.getServer("testserver"));
|
||||||
|
assertEquals(Optional.of(info), map.getServer("TESTSERVER"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsRepeatedRegisterAttempts() {
|
||||||
|
ServerMap map = new ServerMap();
|
||||||
|
ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS);
|
||||||
|
map.register(info);
|
||||||
|
|
||||||
|
ServerInfo willReject = new ServerInfo("TESTSERVER", TEST_ADDRESS);
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> map.register(willReject));
|
||||||
|
}
|
||||||
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren