added rate limiting, added connections per ip limit

This commit is contained in:
LAX1DUDE 2022-08-13 20:00:26 -07:00
parent f26e1b8f6d
commit 7e3aee8699
5 changed files with 477 additions and 109 deletions

View File

@ -20,12 +20,14 @@ public class EaglerSPClient {
public final long createdOn; public final long createdOn;
public boolean serverNotifiedOfClose = false; public boolean serverNotifiedOfClose = false;
public LoginState state = LoginState.INIT; public LoginState state = LoginState.INIT;
public final String address;
EaglerSPClient(WebSocket sock, EaglerSPServer srv, String id) { EaglerSPClient(WebSocket sock, EaglerSPServer srv, String id, String addr) {
this.socket = sock; this.socket = sock;
this.server = srv; this.server = srv;
this.id = id; this.id = id;
this.createdOn = System.currentTimeMillis(); this.createdOn = System.currentTimeMillis();
this.address = addr;
} }
public void send(IPacket packet) { public void send(IPacket packet) {

View File

@ -18,6 +18,7 @@ import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer; import org.java_websocket.server.WebSocketServer;
import net.lax1dude.eaglercraft.sp.relay.RateLimiter.RateLimit;
import net.lax1dude.eaglercraft.sp.relay.pkt.*; import net.lax1dude.eaglercraft.sp.relay.pkt.*;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld; import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld;
@ -26,6 +27,9 @@ public class EaglerSPRelay extends WebSocketServer {
public static EaglerSPRelay instance; public static EaglerSPRelay instance;
public static final EaglerSPRelayConfig config = new EaglerSPRelayConfig(); public static final EaglerSPRelayConfig config = new EaglerSPRelayConfig();
private static RateLimiter pingRateLimiter = null;
private static RateLimiter worldRateLimiter = null;
public static final DebugLogger logger = DebugLogger.getLogger("EaglerSPRelay"); public static final DebugLogger logger = DebugLogger.getLogger("EaglerSPRelay");
public static void main(String[] args) throws IOException, InterruptedException { public static void main(String[] args) throws IOException, InterruptedException {
@ -35,9 +39,24 @@ public class EaglerSPRelay extends WebSocketServer {
logger.debug("Debug logging enabled"); logger.debug("Debug logging enabled");
} }
} }
logger.info("Starting EaglerSPRelay version {}...", Constants.versionName); logger.info("Starting EaglerSPRelay version {}...", Constants.versionName);
config.load(new File("relayConfig.ini")); config.load(new File("relayConfig.ini"));
if(config.isPingRateLimitEnable()) {
pingRateLimiter = new RateLimiter(config.getPingRateLimitPeriod() * 1000,
config.getPingRateLimitLimit(), config.getPingRateLimitLockoutLimit(),
config.getPingRateLimitLockoutDuration() * 1000);
}
if(config.isWorldRateLimitEnable()) {
worldRateLimiter = new RateLimiter(config.getWorldRateLimitPeriod() * 1000,
config.getWorldRateLimitLimit(), config.getWorldRateLimitLockoutLimit(),
config.getWorldRateLimitLockoutDuration() * 1000);
}
EaglerSPRelayConfigRelayList.loadRelays(new File("relays.txt")); EaglerSPRelayConfigRelayList.loadRelays(new File("relays.txt"));
logger.info("Starting WebSocket Server..."); logger.info("Starting WebSocket Server...");
instance = new EaglerSPRelay(new InetSocketAddress(config.getAddress(), config.getPort())); instance = new EaglerSPRelay(new InetSocketAddress(config.getAddress(), config.getPort()));
instance.setConnectionLostTimeout(20); instance.setConnectionLostTimeout(20);
@ -45,6 +64,7 @@ public class EaglerSPRelay extends WebSocketServer {
instance.start(); instance.start();
Thread tickThread = new Thread((() -> { Thread tickThread = new Thread((() -> {
int rateLimitUpdateCounter = 0;
while(true) { while(true) {
try { try {
long millis = System.currentTimeMillis(); long millis = System.currentTimeMillis();
@ -52,7 +72,7 @@ public class EaglerSPRelay extends WebSocketServer {
Iterator<Entry<WebSocket,PendingConnection>> itr = pendingConnections.entrySet().iterator(); Iterator<Entry<WebSocket,PendingConnection>> itr = pendingConnections.entrySet().iterator();
while(itr.hasNext()) { while(itr.hasNext()) {
Entry<WebSocket,PendingConnection> etr = itr.next(); Entry<WebSocket,PendingConnection> etr = itr.next();
if(millis - etr.getValue().openTime > 1000l) { if(millis - etr.getValue().openTime > 500l) {
etr.getKey().close(); etr.getKey().close();
itr.remove(); itr.remove();
} }
@ -62,16 +82,25 @@ public class EaglerSPRelay extends WebSocketServer {
Iterator<EaglerSPClient> itr = clientConnections.values().iterator(); Iterator<EaglerSPClient> itr = clientConnections.values().iterator();
while(itr.hasNext()) { while(itr.hasNext()) {
EaglerSPClient cl = itr.next(); EaglerSPClient cl = itr.next();
if(millis - cl.createdOn > 500l) { if(millis - cl.createdOn > 5000l) {
cl.disconnect(IPacketFEDisconnectClient.TYPE_TIMEOUT, "Took too long to connect!"); cl.disconnect(IPacketFEDisconnectClient.TYPE_TIMEOUT, "Took too long to connect!");
} }
} }
} }
if(++rateLimitUpdateCounter > 300) {
if(pingRateLimiter != null) {
pingRateLimiter.update();
}
if(worldRateLimiter != null) {
worldRateLimiter.update();
}
rateLimitUpdateCounter = 0;
}
}catch(Throwable t) { }catch(Throwable t) {
logger.error("Error in update loop!"); logger.error("Error in update loop!");
logger.error(t); logger.error(t);
} }
Util.sleep(50l); Util.sleep(100l);
} }
}), "Relay Tick"); }), "Relay Tick");
tickThread.setDaemon(true); tickThread.setDaemon(true);
@ -114,6 +143,7 @@ public class EaglerSPRelay extends WebSocketServer {
private static final Map<WebSocket,EaglerSPClient> clientConnections = new HashMap(); private static final Map<WebSocket,EaglerSPClient> clientConnections = new HashMap();
private static final Map<String,EaglerSPServer> serverCodes = new HashMap(); private static final Map<String,EaglerSPServer> serverCodes = new HashMap();
private static final Map<WebSocket,EaglerSPServer> serverConnections = new HashMap(); private static final Map<WebSocket,EaglerSPServer> serverConnections = new HashMap();
private static final Map<String,List<EaglerSPClient>> clientAddressSets = new HashMap();
private static final Map<String,List<EaglerSPServer>> serverAddressSets = new HashMap(); private static final Map<String,List<EaglerSPServer>> serverAddressSets = new HashMap();
@Override @Override
@ -131,7 +161,32 @@ public class EaglerSPRelay extends WebSocketServer {
}else { }else {
addr = arg0.getRemoteSocketAddress().getAddress().getHostAddress().toLowerCase(); addr = arg0.getRemoteSocketAddress().getAddress().getHostAddress().toLowerCase();
} }
int totalCons = 0;
synchronized(pendingConnections) {
Iterator<PendingConnection> pendingItr = pendingConnections.values().iterator();
while(pendingItr.hasNext()) {
if(pendingItr.next().address.equals(addr)) {
++totalCons;
}
}
}
synchronized(clientAddressSets) {
List<EaglerSPClient> lst = clientAddressSets.get(addr);
if(lst != null) {
totalCons += lst.size();
}
}
if(totalCons >= config.getConnectionsPerIP()) {
logger.debug("[{}]: Too many connections are open on this address", arg0.getAttachment());
arg0.send(IPacketFEDisconnectClient.ratelimitPacketTooMany);
arg0.close();
return;
}
arg0.setAttachment(addr); arg0.setAttachment(addr);
PendingConnection waiting = new PendingConnection(millis, addr); PendingConnection waiting = new PendingConnection(millis, addr);
logger.debug("[{}]: Connection opened", arg0.getRemoteSocketAddress()); logger.debug("[{}]: Connection opened", arg0.getRemoteSocketAddress());
synchronized(pendingConnections) { synchronized(pendingConnections) {
@ -165,6 +220,21 @@ public class EaglerSPRelay extends WebSocketServer {
return; return;
} }
if(ipkt.connectionType == 0x01) { if(ipkt.connectionType == 0x01) {
if(!rateLimit(worldRateLimiter, arg0, waiting.address)) {
logger.debug("[{}]: Got world ratelimited", arg0.getAttachment());
return;
}
synchronized(serverAddressSets) {
List<EaglerSPServer> lst = serverAddressSets.get(waiting.address);
if(lst != null) {
if(lst.size() >= config.getWorldsPerIP()) {
logger.debug("[{}]: Too many worlds are open on this address", arg0.getAttachment());
arg0.send(IPacketFEDisconnectClient.ratelimitPacketTooMany);
arg0.close();
return;
}
}
}
logger.debug("[{}]: Connected as a server", arg0.getAttachment()); logger.debug("[{}]: Connected as a server", arg0.getAttachment());
EaglerSPServer srv; EaglerSPServer srv;
synchronized(serverCodes) { synchronized(serverCodes) {
@ -200,57 +270,71 @@ public class EaglerSPRelay extends WebSocketServer {
} }
srv.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers)); srv.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
logger.debug("[{}][Relay -> Server] PKT 0x01: Send ICE server list to server", arg0.getAttachment()); logger.debug("[{}][Relay -> Server] PKT 0x01: Send ICE server list to server", arg0.getAttachment());
}else if(ipkt.connectionType == 0x02) { }else {
String code = ipkt.connectionCode; if(!rateLimit(pingRateLimiter, arg0, waiting.address)) {
logger.debug("[{}]: Connected as a client, requested server code: {}", arg0.getAttachment(), code); logger.debug("[{}]: Got ping ratelimited", arg0.getAttachment());
if(code.length() != config.getCodeLength()) { return;
logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting"); }
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_CODE_LENGTH, if(ipkt.connectionType == 0x02) {
"The join code is the wrong length, it should be " + config.getCodeLength() + " chars long"))); String code = ipkt.connectionCode;
logger.debug("[{}]: Connected as a client, requested server code: {}", arg0.getAttachment(), code);
if(code.length() != config.getCodeLength()) {
logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting");
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_CODE_LENGTH,
"The join code is the wrong length, it should be " + config.getCodeLength() + " chars long")));
arg0.close();
}else {
EaglerSPServer srv;
synchronized(serverCodes) {
srv = serverCodes.get(code);
}
if(srv == null) {
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INCORRECT_CODE,
"Invalid code, no LAN world found!")));
arg0.close();
return;
}
String id;
EaglerSPClient cl;
synchronized(clientIds) {
int j = 0;
do {
id = EaglerSPClient.generateClientId();
}while(clientIds.containsKey(id));
cl = new EaglerSPClient(arg0, srv, id, waiting.address);
clientIds.put(id, cl);
ipkt.connectionCode = id;
arg0.send(IPacket.writePacket(ipkt));
srv.handleNewClient(cl);
}
synchronized(clientConnections) {
clientConnections.put(arg0, cl);
}
synchronized(clientAddressSets) {
List<EaglerSPClient> lst = clientAddressSets.get(cl.address);
if(lst == null) {
lst = new ArrayList();
clientAddressSets.put(cl.address, lst);
}
lst.add(cl);
}
cl.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", arg0.getAttachment());
}
}else if(ipkt.connectionType == 0x02) {
logger.debug("[{}]: Pinging the server", arg0.getAttachment());
arg0.send(IPacket.writePacket(new IPacket69Pong(Constants.protocolVersion, config.getComment(), Constants.versionBrand)));
arg0.close();
}else if(ipkt.connectionType == 0x03) {
logger.debug("[{}]: Polling the server for other worlds", arg0.getAttachment());
arg0.send(IPacket.writePacket(new IPacket07LocalWorlds(getLocalWorlds(waiting.address))));
arg0.close(); arg0.close();
}else { }else {
EaglerSPServer srv; logger.debug("[{}]: Unknown connection type: {}", arg0.getAttachment(), ipkt.connectionType);
synchronized(serverCodes) { arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
srv = serverCodes.get(code); "Unexpected Init Packet")));
} arg0.close();
if(srv == null) {
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INCORRECT_CODE,
"Invalid code, no LAN world found!")));
arg0.close();
return;
}
String id;
EaglerSPClient cl;
synchronized(clientIds) {
int j = 0;
do {
id = EaglerSPClient.generateClientId();
}while(clientIds.containsKey(id));
cl = new EaglerSPClient(arg0, srv, id);
clientIds.put(id, cl);
ipkt.connectionCode = id;
arg0.send(IPacket.writePacket(ipkt));
srv.handleNewClient(cl);
}
synchronized(clientConnections) {
clientConnections.put(arg0, cl);
}
cl.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", arg0.getAttachment());
} }
}else if(ipkt.connectionType == 0x02) {
logger.debug("[{}]: Pinging the server", arg0.getAttachment());
arg0.send(IPacket.writePacket(new IPacket69Pong(Constants.protocolVersion, config.getComment(), Constants.versionBrand)));
arg0.close();
}else if(ipkt.connectionType == 0x03) {
logger.debug("[{}]: Polling the server for other worlds", arg0.getAttachment());
arg0.send(IPacket.writePacket(new IPacket07LocalWorlds(getLocalWorlds(waiting.address))));
arg0.close();
}else {
logger.debug("[{}]: Unknown connection type: {}", arg0.getAttachment(), ipkt.connectionType);
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
"Unexpected Init Packet")));
arg0.close();
} }
}else { }else {
logger.debug("[{}]: Pending connection did not send a 0x00 packet to identify " logger.debug("[{}]: Pending connection did not send a 0x00 packet to identify "
@ -314,6 +398,15 @@ public class EaglerSPRelay extends WebSocketServer {
synchronized(serverCodes) { synchronized(serverCodes) {
serverCodes.remove(srv.code); serverCodes.remove(srv.code);
} }
synchronized(serverAddressSets) {
List<EaglerSPServer> lst = serverAddressSets.get(srv.serverAddress);
if(lst != null) {
lst.remove(srv);
if(lst.size() == 0) {
serverAddressSets.remove(srv.serverAddress);
}
}
}
ArrayList<EaglerSPClient> clientList; ArrayList<EaglerSPClient> clientList;
synchronized(clientConnections) { synchronized(clientConnections) {
clientList = new ArrayList(clientConnections.values()); clientList = new ArrayList(clientConnections.values());
@ -326,18 +419,20 @@ public class EaglerSPRelay extends WebSocketServer {
cl.socket.close(); cl.socket.close();
} }
} }
synchronized(serverAddressSets) {
List<EaglerSPServer> lst = serverAddressSets.get(srv.serverAddress);
lst.remove(srv);
if(lst.size() == 0) {
serverAddressSets.remove(srv.serverAddress);
}
}
}else { }else {
EaglerSPClient cl; EaglerSPClient cl;
synchronized(clientConnections) { synchronized(clientConnections) {
cl = clientConnections.remove(arg0); cl = clientConnections.remove(arg0);
} }
synchronized(clientAddressSets) {
List<EaglerSPClient> lst = clientAddressSets.get(cl.address);
if(lst != null) {
lst.remove(cl);
if(lst.size() == 0) {
clientAddressSets.remove(cl.address);
}
}
}
if(cl != null) { if(cl != null) {
logger.debug("[{}]: Client closed, id: {}", arg0.getAttachment(), cl.id); logger.debug("[{}]: Client closed, id: {}", arg0.getAttachment(), cl.id);
synchronized(clientIds) { synchronized(clientIds) {
@ -375,5 +470,30 @@ public class EaglerSPRelay extends WebSocketServer {
} }
return lst; return lst;
} }
private boolean rateLimit(RateLimiter limiter, WebSocket sock, String addr) {
if(limiter != null) {
RateLimit l = limiter.limit(addr);
if(l == RateLimit.NONE) {
return true;
}else if(l == RateLimit.LIMIT) {
sock.send(IPacketFEDisconnectClient.ratelimitPacketBlock);
sock.close();
return false;
}else if(l == RateLimit.LIMIT_NOW_LOCKOUT) {
sock.send(IPacketFEDisconnectClient.ratelimitPacketBlockLock);
sock.close();
return false;
}else if(l == RateLimit.LOCKOUT) {
sock.send(IPacketFEDisconnectClient.ratelimitPacketLocked);
sock.close();
return false;
}else {
return true; // ?
}
}else {
return true;
}
}
} }

View File

@ -17,12 +17,22 @@ public class EaglerSPRelayConfig {
private int codeLength = 5; private int codeLength = 5;
private String codeChars = "abcdefghijklmnopqrstuvwxyz0123456789$%&*+?!"; private String codeChars = "abcdefghijklmnopqrstuvwxyz0123456789$%&*+?!";
private boolean codeMixCase = false; private boolean codeMixCase = false;
private int connectionsPerIP = 256;
private boolean rateLimitEnable = false; private int connectionsPerIP = 128;
private int rateLimitPeriod = 128; private int worldsPerIP = 32;
private int rateLimitLimit = 48;
private int rateLimitLockoutLimit = 64; private boolean openRateLimitEnable = true;
private int rateLimitLockoutDuration = 600; private int openRateLimitPeriod = 192;
private int openRateLimitLimit = 32;
private int openRateLimitLockoutLimit = 48;
private int openRateLimitLockoutDuration = 600;
private boolean pingRateLimitEnable = true;
private int pingRateLimitPeriod = 256;
private int pingRateLimitLimit = 128;
private int pingRateLimitLockoutLimit = 192;
private int pingRateLimitLockoutDuration = 300;
private String originWhitelist = ""; private String originWhitelist = "";
private String[] originWhitelistArray = new String[0]; private String[] originWhitelistArray = new String[0];
private boolean enableRealIpHeader = false; private boolean enableRealIpHeader = false;
@ -35,11 +45,17 @@ public class EaglerSPRelayConfig {
}else { }else {
EaglerSPRelay.logger.info("Loading config file: {}", conf.getAbsoluteFile()); EaglerSPRelay.logger.info("Loading config file: {}", conf.getAbsoluteFile());
boolean gotPort = false, gotCodeLength = false, gotCodeChars = false; boolean gotPort = false, gotCodeLength = false, gotCodeChars = false;
boolean gotCodeMixCase = false, gotConnectionsPerIP = false; boolean gotCodeMixCase = false;
boolean gotRateLimitEnable = false, gotRateLimitPeriod = false; boolean gotConnectionsPerIP = false, gotWorldsPerIP = false,
boolean gotRateLimitLimit = false, gotRateLimitLockoutLimit = false; gotOpenRateLimitEnable = false, gotOpenRateLimitPeriod = false,
boolean gotRateLimitLockoutDuration = false, gotOriginWhitelist = false; gotOpenRateLimitLimit = false, gotOpenRateLimitLockoutLimit = false,
boolean gotEnableRealIpHeader = false, gotAddress = false, gotComment = false; gotOpenRateLimitLockoutDuration = false;
boolean gotPingRateLimitEnable = false, gotPingRateLimitPeriod = false,
gotPingRateLimitLimit = false, gotPingRateLimitLockoutLimit = false,
gotPingRateLimitLockoutDuration = false;
boolean gotOriginWhitelist = false, gotEnableRealIpHeader = false,
gotAddress = false, gotComment = false;
Throwable t2 = null; Throwable t2 = null;
try(BufferedReader reader = new BufferedReader(new FileReader(conf))) { try(BufferedReader reader = new BufferedReader(new FileReader(conf))) {
String s; String s;
@ -86,6 +102,66 @@ public class EaglerSPRelayConfig {
t2 = t; t2 = t;
break; break;
} }
}else if(ss[0].equalsIgnoreCase("worlds-per-ip")) {
try {
worldsPerIP = Integer.parseInt(ss[1]);
gotWorldsPerIP = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid worlds-per-ip {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("world-ratelimit-enable")) {
try {
openRateLimitEnable = getBooleanValue(ss[1]);
gotOpenRateLimitEnable = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid world-ratelimit-enable {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("world-ratelimit-period")) {
try {
openRateLimitPeriod = Integer.parseInt(ss[1]);
gotOpenRateLimitPeriod = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid world-ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("world-ratelimit-limit")) {
try {
openRateLimitLimit = Integer.parseInt(ss[1]);
gotOpenRateLimitLimit = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid world-ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("world-ratelimit-lockout-limit")) {
try {
openRateLimitLockoutLimit = Integer.parseInt(ss[1]);
gotOpenRateLimitLockoutLimit = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid world-ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("world-ratelimit-lockout-duration")) {
try {
openRateLimitLockoutDuration = Integer.parseInt(ss[1]);
gotOpenRateLimitLockoutDuration = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid world-ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("connections-per-ip")) { }else if(ss[0].equalsIgnoreCase("connections-per-ip")) {
try { try {
connectionsPerIP = Integer.parseInt(ss[1]); connectionsPerIP = Integer.parseInt(ss[1]);
@ -96,52 +172,52 @@ public class EaglerSPRelayConfig {
t2 = t; t2 = t;
break; break;
} }
}else if(ss[0].equalsIgnoreCase("ratelimit-enable")) { }else if(ss[0].equalsIgnoreCase("ping-ratelimit-enable")) {
try { try {
rateLimitEnable = getBooleanValue(ss[1]); pingRateLimitEnable = getBooleanValue(ss[1]);
gotRateLimitEnable = true; gotPingRateLimitEnable = true;
}catch(Throwable t) { }catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid rate-limit-enable {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn("Invalid ping-ratelimit-enable {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t); EaglerSPRelay.logger.warn(t);
t2 = t; t2 = t;
break; break;
} }
}else if(ss[0].equalsIgnoreCase("ratelimit-period")) { }else if(ss[0].equalsIgnoreCase("ping-ratelimit-period")) {
try { try {
rateLimitPeriod = Integer.parseInt(ss[1]); pingRateLimitPeriod = Integer.parseInt(ss[1]);
gotRateLimitPeriod = true; gotPingRateLimitPeriod = true;
}catch(Throwable t) { }catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn("Invalid ping-ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t); EaglerSPRelay.logger.warn(t);
t2 = t; t2 = t;
break; break;
} }
}else if(ss[0].equalsIgnoreCase("ratelimit-limit")) { }else if(ss[0].equalsIgnoreCase("ping-ratelimit-limit")) {
try { try {
rateLimitLimit = Integer.parseInt(ss[1]); pingRateLimitLimit = Integer.parseInt(ss[1]);
gotRateLimitLimit = true; gotPingRateLimitLimit = true;
}catch(Throwable t) { }catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn("Invalid ping-ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t); EaglerSPRelay.logger.warn(t);
t2 = t; t2 = t;
break; break;
} }
}else if(ss[0].equalsIgnoreCase("ratelimit-lockout-limit")) { }else if(ss[0].equalsIgnoreCase("ping-ratelimit-lockout-limit")) {
try { try {
rateLimitLockoutLimit = Integer.parseInt(ss[1]); pingRateLimitLockoutLimit = Integer.parseInt(ss[1]);
gotRateLimitLockoutLimit = true; gotPingRateLimitLockoutLimit = true;
}catch(Throwable t) { }catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn("Invalid ping-ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t); EaglerSPRelay.logger.warn(t);
t2 = t; t2 = t;
break; break;
} }
}else if(ss[0].equalsIgnoreCase("ratelimit-lockout-duration")) { }else if(ss[0].equalsIgnoreCase("ping-ratelimit-lockout-duration")) {
try { try {
rateLimitLockoutDuration = Integer.parseInt(ss[1]); pingRateLimitLockoutDuration = Integer.parseInt(ss[1]);
gotRateLimitLockoutDuration = true; gotPingRateLimitLockoutDuration = true;
}catch(Throwable t) { }catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn("Invalid ping-ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t); EaglerSPRelay.logger.warn(t);
t2 = t; t2 = t;
break; break;
@ -174,10 +250,14 @@ public class EaglerSPRelayConfig {
t2 = t; t2 = t;
} }
if(t2 != null || !gotPort || !gotCodeLength || !gotCodeChars || if(t2 != null || !gotPort || !gotCodeLength || !gotCodeChars ||
!gotCodeMixCase || !gotConnectionsPerIP || !gotRateLimitEnable || !gotCodeMixCase || !gotWorldsPerIP || !gotOpenRateLimitEnable ||
!gotRateLimitPeriod || !gotRateLimitLimit || !gotRateLimitLockoutLimit || !gotOpenRateLimitPeriod || !gotOpenRateLimitLimit ||
!gotRateLimitLockoutDuration || !gotOriginWhitelist || !gotOpenRateLimitLockoutLimit || !gotOpenRateLimitLockoutDuration ||
!gotEnableRealIpHeader || !gotAddress || !gotComment) { !gotConnectionsPerIP || !gotPingRateLimitEnable ||
!gotPingRateLimitPeriod || !gotPingRateLimitLimit ||
!gotPingRateLimitLockoutLimit || !gotPingRateLimitLockoutDuration ||
!gotOriginWhitelist || !gotEnableRealIpHeader || !gotAddress ||
!gotComment) {
EaglerSPRelay.logger.warn("Updating config file: {}", conf.getAbsoluteFile()); EaglerSPRelay.logger.warn("Updating config file: {}", conf.getAbsoluteFile());
save(conf); save(conf);
} }
@ -204,11 +284,17 @@ public class EaglerSPRelayConfig {
w.println("code-chars=" + codeChars); w.println("code-chars=" + codeChars);
w.println("code-mix-case=" + codeMixCase); w.println("code-mix-case=" + codeMixCase);
w.println("connections-per-ip=" + connectionsPerIP); w.println("connections-per-ip=" + connectionsPerIP);
w.println("ratelimit-enable=" + rateLimitEnable); w.println("ping-ratelimit-enable=" + pingRateLimitEnable);
w.println("ratelimit-period=" + rateLimitPeriod); w.println("ping-ratelimit-period=" + pingRateLimitPeriod);
w.println("ratelimit-limit=" + rateLimitLimit); w.println("ping-ratelimit-limit=" + pingRateLimitLimit);
w.println("ratelimit-lockout-limit=" + rateLimitLockoutLimit); w.println("ping-ratelimit-lockout-limit=" + pingRateLimitLockoutLimit);
w.println("ratelimit-lockout-duration=" + rateLimitLockoutDuration); w.println("ping-ratelimit-lockout-duration=" + pingRateLimitLockoutDuration);
w.println("worlds-per-ip=" + worldsPerIP);
w.println("world-ratelimit-enable=" + openRateLimitEnable);
w.println("world-ratelimit-period=" + openRateLimitPeriod);
w.println("world-ratelimit-limit=" + openRateLimitLimit);
w.println("world-ratelimit-lockout-limit=" + openRateLimitLockoutLimit);
w.println("world-ratelimit-lockout-duration=" + openRateLimitLockoutDuration);
w.println("origin-whitelist=" + originWhitelist); w.println("origin-whitelist=" + originWhitelist);
w.println("enable-real-ip-header=" + enableRealIpHeader); w.println("enable-real-ip-header=" + enableRealIpHeader);
w.print("server-comment=" + serverComment); w.print("server-comment=" + serverComment);
@ -252,24 +338,48 @@ public class EaglerSPRelayConfig {
return connectionsPerIP; return connectionsPerIP;
} }
public boolean isRateLimitEnable() { public boolean isPingRateLimitEnable() {
return rateLimitEnable; return pingRateLimitEnable;
} }
public int getRateLimitPeriod() { public int getPingRateLimitPeriod() {
return rateLimitPeriod; return pingRateLimitPeriod;
} }
public int getRateLimitLimit() { public int getPingRateLimitLimit() {
return rateLimitLimit; return pingRateLimitLimit;
} }
public int getRateLimitLockoutLimit() { public int getPingRateLimitLockoutLimit() {
return rateLimitLockoutLimit; return pingRateLimitLockoutLimit;
} }
public int getRateLimitLockoutDuration() { public int getPingRateLimitLockoutDuration() {
return rateLimitLockoutDuration; return pingRateLimitLockoutDuration;
}
public int getWorldsPerIP() {
return worldsPerIP;
}
public boolean isWorldRateLimitEnable() {
return openRateLimitEnable;
}
public int getWorldRateLimitPeriod() {
return openRateLimitPeriod;
}
public int getWorldRateLimitLimit() {
return openRateLimitLimit;
}
public int getWorldRateLimitLockoutLimit() {
return openRateLimitLockoutLimit;
}
public int getWorldRateLimitLockoutDuration() {
return openRateLimitLockoutDuration;
} }
public String getOriginWhitelist() { public String getOriginWhitelist() {
@ -279,6 +389,23 @@ public class EaglerSPRelayConfig {
public String[] getOriginWhitelistArray() { public String[] getOriginWhitelistArray() {
return originWhitelistArray; return originWhitelistArray;
} }
public boolean getIsWhitelisted(String domain) {
domain = domain.toLowerCase();
for(int i = 0; i < originWhitelistArray.length; ++i) {
String etr = originWhitelistArray[i].toLowerCase();
if(etr.startsWith("*")) {
if(domain.endsWith(etr.substring(1))) {
return true;
}
}else {
if(domain.equals(etr)) {
return true;
}
}
}
return false;
}
public boolean isEnableRealIpHeader() { public boolean isEnableRealIpHeader() {
return enableRealIpHeader; return enableRealIpHeader;

View File

@ -0,0 +1,113 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class RateLimiter {
private final int period;
private final int limit;
private final int lockoutLimit;
private final int lockoutDuration;
private class RateLimitEntry {
protected long timer;
protected int count;
protected long lockedTimer;
protected boolean locked;
protected RateLimitEntry() {
timer = System.currentTimeMillis();
count = 0;
lockedTimer = 0l;
locked = false;
}
protected void update() {
long millis = System.currentTimeMillis();
if(locked) {
if(millis - lockedTimer > RateLimiter.this.lockoutDuration) {
timer = millis;
count = 0;
lockedTimer = 0l;
locked = false;
}
}else {
long p = RateLimiter.this.period;
int breaker = 0;
while(millis - timer > p) {
timer += p;
--count;
if(count < 0 || ++breaker > 100) {
timer = millis;
count = 0;
break;
}
}
}
}
}
public static enum RateLimit {
NONE, LIMIT, LIMIT_NOW_LOCKOUT, LOCKOUT;
}
private final Map<String, RateLimitEntry> limiters = new HashMap();
public RateLimiter(int period, int limit, int lockoutLimit, int lockoutDuration) {
this.period = period;
this.limit = limit;
this.lockoutLimit = lockoutLimit;
this.lockoutDuration = lockoutDuration;
}
public RateLimit limit(String addr) {
synchronized(this) {
RateLimitEntry etr = limiters.get(addr);
if(etr == null) {
etr = new RateLimitEntry();
limiters.put(addr, etr);
}else {
etr.update();
}
if(etr.locked) {
return RateLimit.LOCKOUT;
}
++etr.count;
if(etr.count >= lockoutLimit) {
etr.count = 0;
etr.locked = true;
etr.lockedTimer = System.currentTimeMillis();
return RateLimit.LIMIT_NOW_LOCKOUT;
}else if(etr.count > limit) {
return RateLimit.LIMIT;
}else {
return RateLimit.NONE;
}
}
}
public void update() {
synchronized(this) {
Iterator<RateLimitEntry> itr = limiters.values().iterator();
while(itr.hasNext()) {
if(itr.next().count == 0) {
itr.remove();
}
}
}
}
public void reset() {
synchronized(this) {
limiters.clear();
}
}
}

View File

@ -3,6 +3,7 @@ package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
public class IPacketFEDisconnectClient extends IPacket { public class IPacketFEDisconnectClient extends IPacket {
@ -41,5 +42,10 @@ public class IPacketFEDisconnectClient extends IPacket {
public int packetLength() { public int packetLength() {
return -1; return -1;
} }
public static final ByteBuffer ratelimitPacketTooMany = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x00 });
public static final ByteBuffer ratelimitPacketBlock = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x01 });
public static final ByteBuffer ratelimitPacketBlockLock = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x02 });
public static final ByteBuffer ratelimitPacketLocked = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x03 });
} }