Add connection attempt rate-limiting.

This commit is contained in:
Andrew Steinborn 2018-08-09 03:23:27 -04:00
parent db8b7c807c
commit 254508a5cf
6 changed files with 123 additions and 0 deletions

View File

@ -21,6 +21,7 @@ import com.velocitypowered.proxy.command.CommandManager;
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.Ratelimiter;
import com.velocitypowered.proxy.util.ServerMap;
import io.netty.bootstrap.Bootstrap;
import net.kyori.text.Component;
@ -71,6 +72,7 @@ public class VelocityServer implements ProxyServer {
return true;
}
};
private final Ratelimiter ipAttemptLimiter = new Ratelimiter(3000); // TODO: Configurable.
private VelocityServer() {
commandManager.registerCommand("velocity", new VelocityCommand());
@ -162,6 +164,10 @@ public class VelocityServer implements ProxyServer {
return httpClient;
}
public Ratelimiter getIpAttemptLimiter() {
return ipAttemptLimiter;
}
public boolean registerConnection(ConnectedPlayer connection) {
String lowerName = connection.getUsername().toLowerCase(Locale.US);
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {

View File

@ -15,6 +15,7 @@ import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.format.TextColor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
public class HandshakeSessionHandler implements MinecraftSessionHandler {
@ -50,6 +51,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return;
} 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));
}
break;

View File

@ -91,6 +91,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
VelocityServer.getServer().getHttpClient()
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAcceptAsync(profileResponse -> {
if (inbound.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {

View File

@ -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;
}
}

View File

@ -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()));
}
}

View File

@ -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));
}
}