Compare commits

...
8 Commits
Author SHA1 Message Date
Jones ef8abcec46 Release 2.0.17 (#191) 2024-02-10 11:23:13 +01:00
Jones 1fb38ec8d4 Revert 343d71d18c (#190) 2024-02-10 00:14:02 +01:00
Jones cc5e958e2a Fix issues with the Geyser standalone proxy (#189) 2024-02-09 21:48:02 +01:00
Jones 1437204a15 Fix incompatibility with Velocity builds 347+ (#188) 2024-02-08 17:54:52 +01:00
Jones c96d733920 Use Caffeine cache for blacklist threshold (#187) 2024-02-07 10:29:22 +01:00
Jones c561b3eeaf Fix command delay not working correctly (#186) 2024-02-07 09:13:30 +01:00
Jones 2310864816 Remove deprecated API functions (#185) 2024-02-06 18:41:44 +01:00
Jones 55030c5073 Make blacklist time configurable (#184)
* feat: allow user to customize/configure blacklist time

* feat: implement customizable blacklist threshold

* feat: use caffeine for caching
2024-02-05 15:38:44 +01:00
25 changed files with 224 additions and 134 deletions

No files matched your search

+3 -4
View File
@@ -21,7 +21,6 @@ versioner {
allprojects {
repositories {
mavenCentral() // Lombok
maven(url = "https://jitpack.io/") // simple-yaml
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
}
@@ -45,8 +44,7 @@ allprojects {
implementation("net.kyori:adventure-nbt:$adventureVersion")
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
compileOnly("io.netty:netty-all:4.1.106.Final") // netty
// Library/dependency loading
@@ -73,7 +71,7 @@ tasks {
shadowJar {
// Set the file name of the shadowed jar
archiveFileName.set("Sonar.jar")
archiveFileName.set("${rootProject.name}.jar")
// Remove file timestamps
isPreserveFileTimestamps = false
@@ -98,6 +96,7 @@ tasks {
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.code.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
// We want to load our own Gson dynamically
exclude("com/google/gson/**")
+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.16
version=2.0.17
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
+1 -1
View File
@@ -1,4 +1,4 @@
rootProject.name = "sonar"
rootProject.name = "Sonar"
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
val path = "sonar-$it"
+6 -2
View File
@@ -1,3 +1,7 @@
repositories {
maven(url = "https://jitpack.io/") // simple-yaml
}
dependencies {
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
exclude(group = "org.yaml")
@@ -6,5 +10,5 @@ dependencies {
compileOnly("org.jetbrains:annotations:24.0.1")
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -43,8 +43,8 @@ public final class AttackTracker implements JVMProfiler {
public static final class AttackStatistics {
private final SystemTimer duration = new SystemTimer();
private final SystemTimer timer = new SystemTimer();
private int peakJoinsPerSecond;
private int peakConnectionsPerSecond;
private long peakJoinsPerSecond;
private long peakConnectionsPerSecond;
private double peakProcessCPUUsage;
private long peakProcessMemoryUsage;
// Calculate during-attack-statistics using their deltas
@@ -52,7 +52,7 @@ public final class AttackTracker implements JVMProfiler {
}
public void checkIfUnderAttack() {
final int joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
final long joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
@@ -80,7 +80,7 @@ public final class AttackTracker implements JVMProfiler {
// Update joins per second peak if necessary
currentAttack.peakJoinsPerSecond = joinsPerSecond;
}
final int connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
final long connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
// Update connections per second peak if necessary
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
@@ -113,7 +113,7 @@ public final class AttackTracker implements JVMProfiler {
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklisted().estimatedSize();
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
@@ -17,15 +17,17 @@
package xyz.jonesdev.sonar.api.command;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
@@ -36,7 +38,9 @@ public interface SonarCommand {
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(500))
.build();
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
@@ -47,10 +51,11 @@ public interface SonarCommand {
source.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(source, -1L);
COMMAND_DELAY.cleanUp(); // Clean up the cache
final long timestamp = System.currentTimeMillis();
final long mapTimestamp = COMMAND_DELAY.asMap().getOrDefault(source.getAudience(), -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
@@ -59,7 +64,6 @@ public interface SonarCommand {
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
@@ -67,7 +71,7 @@ public interface SonarCommand {
return;
}
DELAY.put(source);
COMMAND_DELAY.put(source.getAudience(), timestamp);
}
if (args.length > 0) {
@@ -170,6 +170,8 @@ public final class SonarConfiguration {
private int maxPing;
private int readTimeout;
private int reconnectDelay;
private int blacklistTime;
private int blacklistThreshold;
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
@@ -318,7 +320,8 @@ public final class SonarConfiguration {
minPlayersForAttack = clamp(generalConfig.getInt("min-players-for-attack", 8), 2, 1024);
generalConfig.getYaml().setComment("min-attack-duration",
"Amount of time (in milliseconds) that has to pass in order for an attack to be over");
"Amount of time that has to pass in order for an attack to be over"
+ LINE_SEPARATOR + "(This value represents the time in milliseconds: 1 second = 1000 milliseconds)");
minAttackDuration = clamp(generalConfig.getInt("min-attack-duration", 30000), 1000, 900000);
generalConfig.getYaml().setComment("min-attack-threshold",
@@ -327,7 +330,8 @@ public final class SonarConfiguration {
minAttackThreshold = clamp(generalConfig.getInt("min-attack-threshold", 2), 0, 20);
generalConfig.getYaml().setComment("attack-cooldown-delay",
"Amount of time (in milliseconds) that has to pass in order for a new attack to be detected");
"Amount of time that has to pass in order for a new attack to be detected"
+ LINE_SEPARATOR + "(This value represents the time in milliseconds: 1 second = 1000 milliseconds)");
attackCooldownDelay = clamp(generalConfig.getInt("attack-cooldown-delay", 3000), 100, 30000);
generalConfig.getYaml().setComment("log-player-addresses",
@@ -490,7 +494,8 @@ public final class SonarConfiguration {
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 1000);
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration",
"How long should Sonar wait until the player fails the captcha?");
"How long should Sonar wait until the player fails the captcha?"
+ LINE_SEPARATOR + "(This value represents the time in milliseconds: 1 second = 1000 milliseconds)");
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 45000);
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-tries",
@@ -526,7 +531,8 @@ public final class SonarConfiguration {
verification.maxBrandLength = generalConfig.getInt("verification.checks.max-brand-length", 64);
generalConfig.getYaml().setComment("verification.checks.max-ping",
"Ping (in milliseconds) a player has to have in order to timeout");
"Ping a player has to have in order to timeout"
+ LINE_SEPARATOR + "(This value represents the time in milliseconds: 1 second = 1000 milliseconds)");
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping", 10000), 500, 30000);
generalConfig.getYaml().setComment("verification.checks.max-login-packets",
@@ -547,13 +553,25 @@ public final class SonarConfiguration {
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions", false);
generalConfig.getYaml().setComment("verification.read-timeout",
"Amount of time that has to pass before a player times out");
"Amount of time that has to pass before a player times out"
+ LINE_SEPARATOR + "(This value represents the time in milliseconds: 1 second = 1000 milliseconds)");
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout", 3500), 500, 30000);
generalConfig.getYaml().setComment("verification.rejoin-delay",
"Minimum number of rejoin delay during verification");
"How long should a player wait before reconnecting during verification"
+ LINE_SEPARATOR + "(This value represents the time in milliseconds: 1 second = 1000 milliseconds)");
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay", 8000), 0, 100000);
generalConfig.getYaml().setComment("verification.blacklist-time",
"How long should an IP address be denied from logging in when failing the verification too often?"
+ LINE_SEPARATOR + "(This value represents the time in milliseconds: 1 second = 1000 milliseconds)");
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time", 600000), 0, 86400000);
generalConfig.getYaml().setComment("verification.blacklist-threshold",
"How often does a player have to fail the verification in order to be blacklisted?"
+ LINE_SEPARATOR + "(Set this value to 0 to disable the blacklist entirely)");
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold", 2), 0, 100);
generalConfig.getYaml().setComment("verification.whitelisted-protocols",
"List of protocol IDs which are not checked by Sonar (verification bypass)"
+ LINE_SEPARATOR + "You can find the full list of all protocol IDs here:"
@@ -1103,10 +1121,10 @@ public final class SonarConfiguration {
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal",
String.join(" <dark_aqua>╺ ", Arrays.asList(
"%prefix%<gray>CPS <white>%connections-per-second%",
"<gray>Logins/s <white>%logins-per-second%",
"<gray>Verifying <white>%verifying%"
+ " <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s"
+ " <green><bold>%animation%<reset>"))));
"<gray>Logins/s <white>%logins-per-second%",
"<gray>Verifying <white>%verifying%"
+ " <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s"
+ " <green><bold>%animation%<reset>"))));
messagesConfig.getYaml().setComment("verbose.layout.attack",
"Layout for Sonar's actionbar verbose during an attack");
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack",
@@ -1150,7 +1168,8 @@ public final class SonarConfiguration {
" <dark_aqua>▪ <gray>Queued players: <white>%queued%",
"",
"<gray>View more information using " +
"<white><click:run_command:'/sonar verbose'><hover:show_text:'(Click to run)'>/sonar verbose</hover></click>" +
"<white><click:run_command:'/sonar verbose'><hover:show_text:'(Click to run)'>/sonar " +
"verbose</hover></click>" +
"<gray> or " +
"<white><click:run_command:'/sonar stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click>" +
"<gray>.",
@@ -17,12 +17,12 @@
package xyz.jonesdev.sonar.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
@@ -31,7 +31,6 @@ import java.net.InetAddress;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -39,10 +38,14 @@ public final class Fallback {
public static final Fallback INSTANCE = new Fallback();
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
// Map of all connected usernames and their respective IP addresses (used for fast checking)
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
// Only block the player for a few minutes to avoid issues
private final ExpiringCache<InetAddress> blacklisted = Cappuccino.buildExpiring(
10L, TimeUnit.MINUTES, 5000L);
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
@Setter
private Cache<InetAddress, Byte> blacklist;
@Setter
private long blacklistTime;
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
@@ -30,7 +30,7 @@ import java.util.concurrent.ConcurrentHashMap;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class FallbackQueue {
public static final FallbackQueue INSTANCE = new FallbackQueue();
static final FallbackQueue INSTANCE = new FallbackQueue();
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
// Runnable task executed for polling the queue
@@ -17,19 +17,19 @@
package xyz.jonesdev.sonar.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.ExpiringCache;
import java.net.InetAddress;
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class FallbackRatelimiter {
public static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
@Setter
private ExpiringCache<InetAddress> expiringCache;
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
private Cache<InetAddress, Byte> expiringCache;
/**
* We don't want to clean up the cache in the
@@ -37,7 +37,7 @@ public final class FallbackRatelimiter {
* it would take up too many resources.
*/
public void cleanUpCache() {
expiringCache.cleanUp(false);
expiringCache.cleanUp();
}
/**
@@ -49,9 +49,9 @@ public final class FallbackRatelimiter {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean attempt(final @NotNull InetAddress inetAddress) {
// Cache the IP address if it's not already cached
if (!expiringCache.has(inetAddress)) {
if (!expiringCache.asMap().containsKey(inetAddress)) {
// Cache the IP address
expiringCache.put(inetAddress);
expiringCache.put(inetAddress, (byte) 0);
return true;
}
// Deny the connection attempt
@@ -17,15 +17,14 @@
package xyz.jonesdev.sonar.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
@@ -33,7 +32,8 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
import java.util.Objects;
public interface FallbackUser<T> {
@NotNull Fallback getFallback();
@@ -51,19 +51,6 @@ public interface FallbackUser<T> {
@NotNull ProtocolVersion getProtocolVersion();
/**
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Legacy disconnect message string
* @see #disconnect(Component)
*/
@Deprecated
@ApiStatus.ScheduledForRemoval(inVersion = "2.0.17")
default void disconnect(final @NotNull String reason) {
disconnect(Component.text(reason));
}
/**
* Kicks the player from the server with
* the given disconnect message.
@@ -97,7 +84,39 @@ public interface FallbackUser<T> {
}
}
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
Cache<InetAddress, Integer> GLOBAL_FAIL_COUNT = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5)) // expire after 5 minutes
.build();
/**
* Increments the number of times this user (by IP address) has failed the verification
*/
default void incrementFails() {
final InetAddress inetAddress = Objects.requireNonNull(getInetAddress());
final int failCount = getFailedCount();
// Make sure to remove the old values from the cache
if (failCount > 0) {
GLOBAL_FAIL_COUNT.invalidate(inetAddress);
}
GLOBAL_FAIL_COUNT.put(inetAddress, failCount + 1);
}
/**
* @return The number of times this user (by IP address) has failed the verification
* @apiNote Returns 0 if no key/value is present in the cache
*/
default int getFailedCount() {
// Make sure to clean up the cache before we try to get cached values
GLOBAL_FAIL_COUNT.cleanUp();
return GLOBAL_FAIL_COUNT.asMap().getOrDefault(Objects.requireNonNull(getInetAddress()), 0);
}
/**
* Removes all previously failed verifications
*/
default void invalidateFails() {
GLOBAL_FAIL_COUNT.invalidate(Objects.requireNonNull(getInetAddress()));
}
/**
* Disconnects the player who failed the verification
@@ -124,21 +143,32 @@ public interface FallbackUser<T> {
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
// Make sure old entries are removed
PREVIOUS_FAILS.cleanUp();
// Use a label, so we can easily add more code beneath this method
blacklist: {
// Check if the player has too many failed attempts
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
if (blacklistThreshold <= 0) break blacklist;
// Check if the player has too many failed attempts
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
final int failCount = getFailedCount() + 1;
// Now we simply need to check if the threshold is reached
if (failCount >= blacklistThreshold) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
getFallback().getBlacklisted().put(getInetAddress());
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
} else {
// Cache the InetAddress for 3 minutes
PREVIOUS_FAILS.put(getInetAddress().toString());
getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
// Invalidate the cached entry to ensure memory safety
invalidateFails();
break blacklist;
}
// Make sure we increment the number of fails
incrementFails();
}
}
}
@@ -17,12 +17,17 @@
package xyz.jonesdev.sonar.api.statistics;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
public interface Counters {
ExpiringCache<Long> LOGINS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
ExpiringCache<Long> CONNECTIONS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
}
@@ -56,7 +56,7 @@ public final class Notification implements Observable, Counters {
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
@@ -45,10 +45,10 @@ public final class Verbose implements Observable, JVMProfiler, Counters {
// Run action bar verbose
public void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
Sonar.get().getFallback().getBlacklist().cleanUp();
// Clean up all counters
LOGINS_PER_SECOND.cleanUp(false);
CONNECTIONS_PER_SECOND.cleanUp(false);
LOGINS_PER_SECOND.cleanUp();
CONNECTIONS_PER_SECOND.cleanUp();
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) return;
@@ -72,7 +72,7 @@ public final class Verbose implements Observable, JVMProfiler, Counters {
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
+3 -3
View File
@@ -7,7 +7,7 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bukkit")
bukkit {
name = "Sonar"
name = rootProject.name
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
@@ -44,5 +44,5 @@ dependencies {
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
+3 -3
View File
@@ -5,7 +5,7 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = "Sonar"
name = rootProject.name
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
@@ -29,5 +29,5 @@ dependencies {
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -111,7 +111,7 @@ public final class FallbackHandlerBoss extends HandlerBoss {
// Use the same channel wrapper as the "real" HandlerBoss
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(this);
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime());
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
}
}
@@ -143,7 +143,7 @@ public final class FallbackHandlerBoss extends HandlerBoss {
final LoginRequest loginRequest = (LoginRequest) packetWrapper.packet;
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime());
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Fix login packet spam exploit
if (receivedLoginPacket || user != null) {
@@ -168,9 +168,11 @@ public final class FallbackHandlerBoss extends HandlerBoss {
Statistics.TOTAL_TRAFFIC.increment();
// Use the ChannelWrapper's remote address to automatically handle proxy protocol
final InetAddress inetAddress = ((InetSocketAddress) channelWrapper.getRemoteAddress()).getAddress();
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
final InetAddress inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress)) {
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
@@ -182,7 +184,7 @@ public final class FallbackHandlerBoss extends HandlerBoss {
}
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
if (isGeyserConnection(channel, socketAddress)) {
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
super.channelRead(ctx, msg);
+2 -2
View File
@@ -2,5 +2,5 @@ dependencies {
compileOnly(project(":api"))
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -43,6 +43,13 @@ class LibraryLoader {
.version("2.10.1")
.relocate("com{}google{}code{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
.build(),
// Caffeine
Library.builder()
.groupId("com.github.ben-manes.caffeine")
.artifactId("caffeine")
.version("3.1.8")
.relocate("com{}github{}benmanes{}caffeine", "xyz{}jonesdev{}sonar{}libs{}caffeine")
.build(),
// ORMLite
Library.builder()
.groupId("com{}j256{}ormlite")
@@ -18,19 +18,17 @@
package xyz.jonesdev.sonar.common.boot;
import com.alessiodp.libby.LibraryManager;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.SonarSupplier;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.fallback.FallbackRatelimiter;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@@ -39,8 +37,7 @@ import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import java.io.File;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
@Getter
@RequiredArgsConstructor
@@ -121,17 +118,35 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (getVerifiedPlayerController() != null
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabase().getType()) {
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
}
// Prepare cached packets
FallbackPreparer.prepare();
// Update ratelimiter
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS, 250L);
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
// Update ratelimiter cache
getFallback().getRatelimiter().setExpiringCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
.build());
// Update blacklist cache
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
final boolean blacklistExists = getFallback().getBlacklist() != null;
// Make sure the blacklist is only set when we need it to prevent data loss
if (!blacklistExists // Make sure we create a new cache if it doesn't exist yet
|| getFallback().getBlacklistTime() != blacklistTime) {
// Create new cache with the configured blacklist time
getFallback().setBlacklist(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getBlacklistTime()))
.build());
// Store the new blacklist time, so we don't have to reset the blacklist every reload
getFallback().setBlacklistTime(blacklistTime);
// Warn the user about changing the expiry of the blacklist values
if (blacklistExists) {
getLogger().warn("The blacklist has been reset as the duration of entries has changed.");
}
}
// Reinitialize database controller
verifiedPlayerController = new VerifiedPlayerController();
@@ -54,7 +54,7 @@ public final class BlacklistCommand extends Subcommand {
if (inetAddress == null) return;
// Make sure the IP is not blacklisted already
if (SONAR.getFallback().getBlacklisted().has(inetAddress)) {
if (SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
return;
}
@@ -66,7 +66,7 @@ public final class BlacklistCommand extends Subcommand {
}
// Blacklist the given IP address
SONAR.getFallback().getBlacklisted().put(inetAddress);
SONAR.getFallback().getBlacklist().put(inetAddress, (byte) 0);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
.replace("%ip%", rawInetAddress));
break;
@@ -85,20 +85,20 @@ public final class BlacklistCommand extends Subcommand {
if (inetAddress == null) return;
// Make sure the IP is blacklisted
if (!SONAR.getFallback().getBlacklisted().has(inetAddress)) {
if (!SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
return;
}
// Invalidate the cache entry of the blacklisted IP address
SONAR.getFallback().getBlacklisted().invalidate(inetAddress);
SONAR.getFallback().getBlacklist().invalidate(inetAddress);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
}
case "clear": {
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
final long blacklisted = SONAR.getFallback().getBlacklist().estimatedSize();
if (blacklisted == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
@@ -106,7 +106,7 @@ public final class BlacklistCommand extends Subcommand {
}
// Invalidate all cache entries
SONAR.getFallback().getBlacklisted().invalidateAll();
SONAR.getFallback().getBlacklist().invalidateAll();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
break;
@@ -114,7 +114,7 @@ public final class BlacklistCommand extends Subcommand {
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize())));
break;
}
@@ -74,7 +74,7 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getGeneralStatistics()
.replace("%verified%", DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize()))
.replace("%verifying%", DECIMAL_FORMAT.format(SONAR.getFallback().getConnected().size()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize()))
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
.replace("%uptime%", String.format("%dd %dh %dm %ds", days, hours, minutes, seconds % 60L))
.replace("%total_joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
@@ -18,6 +18,7 @@
package xyz.jonesdev.sonar.common.utility.geyser;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
@@ -28,17 +29,16 @@ import java.net.InetSocketAddress;
*/
@UtilityClass
public class GeyserUtil {
// https://github.com/GeyserMC/Floodgate/blob/master/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java#L206
private final AttributeKey<Object> PLAYER_ATTRIBUTE = AttributeKey.valueOf("floodgate-player");
/**
* @param channel Channel of the player
* @return Whether the player is on GeyserMC or not
*/
public boolean isGeyserConnection(final @NotNull Channel channel) {
// https://discord.com/channels/613163671870242838/613170125696270357/1168599123889504266
// Every floodgate player has a channel attribute called "floodgate-player"
// However, this is not safe to use as even Java players sometimes have this attribute.
// Instead, we should check if the port is 0, since TCP didn't resolve a port (UDP)
// Thanks to @dbruni on Discord who reported it in ticket-0072
return ((InetSocketAddress) channel.remoteAddress()).getPort() == 0;
public boolean isGeyserConnection(final @NotNull Channel channel,
final @NotNull InetSocketAddress originalAddress) {
return originalAddress.getPort() == 0 // check for floodgate on the server
|| channel.attr(PLAYER_ATTRIBUTE).get() != null; // check for standalone Geyser proxy connection
}
}
@@ -55,6 +55,7 @@ import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -97,13 +98,13 @@ public final class FallbackListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull ConnectionHandshakeEvent event) throws Throwable {
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime());
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
}
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PreLoginEvent event) throws Throwable {
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime());
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
final LoginInboundConnection inboundConnection = (LoginInboundConnection) event.getConnection();
val initialConnection = (InitialInboundConnection) INITIAL_CONNECTION.invokeExact(inboundConnection);
@@ -121,7 +122,8 @@ public final class FallbackListener {
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
final InetSocketAddress socketAddress = event.getConnection().getRemoteAddress();
final InetAddress inetAddress = socketAddress.getAddress();
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
@@ -130,7 +132,7 @@ public final class FallbackListener {
val activeSessionHandler = mcConnection.getActiveSessionHandler();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress)) {
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
// Mark the connection as dead to avoid unnecessary console logs
markConnectionAsDead(activeSessionHandler);
mcConnection.closeWith(DisconnectPacket.create(
@@ -143,7 +145,7 @@ public final class FallbackListener {
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) return;
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
if (isGeyserConnection(channel, socketAddress)) {
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
event.getUsername(), Sonar.get().getConfig().formatAddress(inetAddress));
return;
@@ -10,7 +10,7 @@
],
"dependencies": [
{
"id": "Geyser-Velocity",
"id": "geyser",
"optional": true
},
{
@@ -18,11 +18,11 @@
"optional": true
},
{
"id": "Protocolize",
"id": "protocolize",
"optional": true
},
{
"id": "ViaVersion",
"id": "viaversion",
"optional": true
}
],