Add a basic rate limiter for incoming connections

Related #347
This commit is contained in:
games647
2020-05-13 19:46:39 +02:00
parent a5777869c8
commit 25b380f74a
10 changed files with 176 additions and 13 deletions

View File

@ -70,9 +70,9 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
}
if (pluginManager.isPluginEnabled("ProtocolSupport")) {
pluginManager.registerEvents(new ProtocolSupportListener(this), this);
pluginManager.registerEvents(new ProtocolSupportListener(this, core.getRateLimiter()), this);
} else if (pluginManager.isPluginEnabled("ProtocolLib")) {
ProtocolLibListener.register(this);
ProtocolLibListener.register(this, core.getRateLimiter());
pluginManager.registerEvents(new SkinApplyListener(this), this);
} else {
logger.warn("Either ProtocolLib or ProtocolSupport have to be installed if you don't use BungeeCord");

View File

@ -6,6 +6,7 @@ import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.RateLimiter;
import java.security.KeyPair;
import java.security.SecureRandom;
@ -24,8 +25,9 @@ public class ProtocolLibListener extends PacketAdapter {
//just create a new once on plugin enable. This used for verify token generation
private final SecureRandom random = new SecureRandom();
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
private final RateLimiter rateLimiter;
public ProtocolLibListener(FastLoginBukkit plugin) {
public ProtocolLibListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
//run async in order to not block the server, because we are making api calls to Mojang
super(params()
.plugin(plugin)
@ -33,13 +35,14 @@ public class ProtocolLibListener extends PacketAdapter {
.optionAsync());
this.plugin = plugin;
this.rateLimiter = rateLimiter;
}
public static void register(FastLoginBukkit plugin) {
public static void register(FastLoginBukkit plugin, RateLimiter rateLimiter) {
//they will be created with a static builder, because otherwise it will throw a NoClassDefFoundError
ProtocolLibrary.getProtocolManager()
.getAsynchronousManager()
.registerAsyncHandler(new ProtocolLibListener(plugin))
.registerAsyncHandler(new ProtocolLibListener(plugin, rateLimiter))
.start(WORKER_THREADS);
}
@ -54,6 +57,11 @@ public class ProtocolLibListener extends PacketAdapter {
Player sender = packetEvent.getPlayer();
PacketType packetType = packetEvent.getPacketType();
if (packetType == START) {
if (!rateLimiter.tryAcquire()) {
plugin.getLog().warn("Rate Limit hit - Ignoring player {}", sender);
return;
}
onLogin(packetEvent, sender);
} else {
onEncryptionBegin(packetEvent, sender);

View File

@ -4,6 +4,7 @@ import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPreLoginEvent;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import com.github.games647.fastlogin.core.shared.event.FastLoginPreLoginEvent;
@ -23,11 +24,13 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
implements Listener {
private final FastLoginBukkit plugin;
private final RateLimiter rateLimiter;
public ProtocolSupportListener(FastLoginBukkit plugin) {
public ProtocolSupportListener(FastLoginBukkit plugin, RateLimiter rateLimiter) {
super(plugin.getCore(), plugin.getCore().getAuthPluginHook());
this.plugin = plugin;
this.rateLimiter = rateLimiter;
}
@EventHandler
@ -36,6 +39,11 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
return;
}
if (!rateLimiter.tryAcquire()) {
plugin.getLog().warn("Rate Limit hit - Ignoring player {}", loginStartEvent.getConnection());
return;
}
String username = loginStartEvent.getConnection().getProfile().getName();
InetSocketAddress address = loginStartEvent.getAddress();

View File

@ -53,7 +53,8 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
}
//events
getProxy().getPluginManager().registerListener(this, new ConnectListener(this));
ConnectListener connectListener = new ConnectListener(this, core.getRateLimiter());
getProxy().getPluginManager().registerListener(this, connectListener);
getProxy().getPluginManager().registerListener(this, new PluginMessageListener(this));
//this is required to listen to incoming messages from the server

View File

@ -1,9 +1,11 @@
package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.task.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.task.ForceLoginTask;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
@ -33,8 +35,11 @@ public class ConnectListener implements Listener {
private final FastLoginBungee plugin;
private final Property[] emptyProperties = {};
public ConnectListener(FastLoginBungee plugin) {
private final RateLimiter rateLimiter;
public ConnectListener(FastLoginBungee plugin, RateLimiter rateLimiter) {
this.plugin = plugin;
this.rateLimiter = rateLimiter;
}
@EventHandler
@ -43,9 +48,14 @@ public class ConnectListener implements Listener {
return;
}
PendingConnection connection = preLoginEvent.getConnection();
if (!rateLimiter.tryAcquire()) {
plugin.getLog().warn("Rate Limit hit - Ignoring player {}", connection);
return;
}
preLoginEvent.registerIntent(plugin);
PendingConnection connection = preLoginEvent.getConnection();
Runnable asyncPremiumCheck = new AsyncPremiumCheck(plugin, preLoginEvent, connection);
plugin.getScheduler().runAsync(asyncPremiumCheck);
}
@ -100,10 +110,15 @@ public class ConnectListener implements Listener {
ProxiedPlayer player = serverConnectedEvent.getPlayer();
Server server = serverConnectedEvent.getServer();
BungeeLoginSession session = plugin.getSession().get(player.getPendingConnection());
if (session == null) {
return;
}
// delay sending force command, because Paper will process the login event asynchronously
// In this case it means that the force command (plugin message) is already received and processed while
// player is still in the login phase and reported to be offline.
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server);
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server, session);
plugin.getScheduler().runAsync(loginTask);
}

View File

@ -10,10 +10,10 @@ import com.github.games647.fastlogin.core.message.LoginActionMessage.Type;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.ForceLoginManagement;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import java.util.UUID;
import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@ -25,8 +25,8 @@ public class ForceLoginTask
private final Server server;
public ForceLoginTask(FastLoginCore<ProxiedPlayer, CommandSender, FastLoginBungee> core,
ProxiedPlayer player, Server server) {
super(core, player, core.getPlugin().getSession().get(player.getPendingConnection()));
ProxiedPlayer player, Server server, BungeeLoginSession session) {
super(core, player, session);
this.server = server;
}

View File

@ -0,0 +1,41 @@
package com.github.games647.fastlogin.core;
/**
* Limit the number of requests with a maximum size. Each requests expires after the specified time making it available
* for another request.
*/
public class RateLimiter {
private final long[] requests;
private final long expireTime;
private int position;
public RateLimiter(int maxLimit, long expireTime) {
this.requests = new long[maxLimit];
this.expireTime = expireTime;
}
/**
* Ask if access is allowed. If so register the request.
*
* @return true if allowed
*/
public boolean tryAcquire() {
// currentTime millis could be expensive on some systems
long now = System.currentTimeMillis();
// after this the request should be expired
long toBeExpired = now - expireTime;
synchronized (this) {
// having synchronized will limit the amount of concurrency a lot
long oldest = requests[position];
if (oldest < toBeExpired) {
requests[position] = now;
position = (position + 1) % requests.length;
return true;
}
return false;
}
}
}

View File

@ -4,6 +4,7 @@ import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.craftapi.resolver.http.RotatingProxySelector;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.RateLimiter;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
@ -55,6 +56,7 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
private Configuration config;
private AuthStorage storage;
private RateLimiter rateLimiter;
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
private AuthPlugin<P> authPlugin;
@ -82,8 +84,12 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
});
} catch (IOException ioEx) {
plugin.getLog().error("Failed to load yaml files", ioEx);
return;
}
int maxCon = config.getInt("anti-bot.connections");
int expireTime = config.getInt("anti-bot.expire");
rateLimiter = new RateLimiter(maxCon, expireTime * 60 * 1_000);
Set<Proxy> proxies = config.getStringList("proxies")
.stream()
.map(HostAndPort::fromString)
@ -217,6 +223,10 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
return authPlugin;
}
public RateLimiter getRateLimiter() {
return rateLimiter;
}
public void setAuthPluginHook(AuthPlugin<P> authPlugin) {
this.authPlugin = authPlugin;
}

View File

@ -5,6 +5,19 @@
# You can access the newest config here:
# https://github.com/games647/FastLogin/blob/master/core/src/main/resources/config.yml
# This a **very** simple anti bot protection. Recommendation is to use a a dedicated program to approach this
# problem. Low level firewalls like iptables, ufw are more efficient than a Minecraft plugin. TCP reverse
# proxies could also be used and offload some work even to different host.
#
# The settings wil limit how many connections this plugin will handle. After hitting this limit. FastLogin will
# completely ignore incoming connections. Effectively there will be no database requests and network requests.
# Therefore auto logins won't be possible.
anti-bot:
# Total number of connections
connections: 200
# Amount of time after the first connection will expire and made available
expire: 5
# Request a premium login without forcing the player to type a command
#
# If you activate autoRegister, this plugin will check/do these points on login:

View File

@ -0,0 +1,67 @@
package com.github.games647.fastlogin.core;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class RateLimiterTest {
private static final long THRESHOLD_MILLI = 10;
/**
* Always expired
*/
@Test
public void allowExpire() throws InterruptedException {
int size = 3;
// run twice the size to fill it first and then test it
RateLimiter rateLimiter = new RateLimiter(size, 0);
for (int i = 0; i < size; i++) {
assertTrue("Filling up", rateLimiter.tryAcquire());
}
for (int i = 0; i < size; i++) {
Thread.sleep(1);
assertTrue("Should be expired", rateLimiter.tryAcquire());
}
}
/**
* Too many requests
*/
@Test
public void shoudBlock() {
int size = 3;
// fill the size
RateLimiter rateLimiter = new RateLimiter(size, TimeUnit.SECONDS.toMillis(30));
for (int i = 0; i < size; i++) {
assertTrue("Filling up", rateLimiter.tryAcquire());
}
assertFalse("Should be full and no entry should be expired", rateLimiter.tryAcquire());
}
/**
* Blocked attempts shouldn't replace existing ones.
*/
@Test
public void blockedNotAdded() throws InterruptedException {
// fill the size - 100ms should be reasonable high
RateLimiter rateLimiter = new RateLimiter(1, 100);
assertTrue("Filling up", rateLimiter.tryAcquire());
Thread.sleep(50);
// still is full - should fail
assertFalse("Expired too early", rateLimiter.tryAcquire());
// wait the remaining time and add a threshold, because
Thread.sleep(50 + THRESHOLD_MILLI);
assertTrue("Request not released", rateLimiter.tryAcquire());
}
}