Compare commits

...
4 Commits
Author SHA1 Message Date
Jones 4474f44b0a Release 2.0.26 (#269) 2024-04-20 10:14:13 +02:00
Jones b159947d6d Recode max accounts per IP check (#268) 2024-04-18 18:39:23 +02:00
Jones a29dfa15e4 Implement max accounts per IP check on Bukkit (#266)
* feat(bukkit): add max online per IP check

* fix: remove debug message haha lol
2024-04-18 17:57:00 +02:00
Jones cb9fe75db8 Allow user to manually verify players (#265)
* feat: add /sonar verified add <ip> <uuid/username>

* fix: remove useless and stupid local ip check

* fix: use correct translation for add message
2024-04-18 17:55:09 +02:00
20 changed files with 172 additions and 197 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.25
version=2.0.26
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
@@ -34,6 +34,7 @@ import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
import java.net.InetAddress;
import java.text.DecimalFormat;
import java.util.UUID;
@@ -52,6 +53,11 @@ public interface Sonar {
*/
@NotNull SonarPlatform getPlatform();
/**
* @return If the amount of players online with the same IP address is greater than a maximum
*/
boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit);
/**
* @return A small wrapper for the plugin logger so we can use the logger everywhere
*/
@@ -53,13 +53,8 @@ public abstract class Subcommand {
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawIP);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
return null;
}
} catch (UnknownHostException exception) {
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
source.sendMessage(SONAR.getConfig().getCommands().getInvalidIpAddress());
return null;
}
return inetAddress;
@@ -256,8 +256,7 @@ public final class SonarConfiguration {
noPermission = formatString(messagesConfig.getString("commands.no-permission"));
commands.incorrectCommandUsage = formatString(messagesConfig.getString("commands.incorrect-usage"));
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address"));
commands.invalidIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only"));
commands.consoleOnly = formatString(messagesConfig.getString("commands.console-only"));
commands.commandCoolDown = formatString(messagesConfig.getString("commands.cool-down"));
@@ -290,6 +289,8 @@ public final class SonarConfiguration {
commands.verifiedCleared = formatString(messagesConfig.getString("commands.verified.cleared"));
commands.verifiedSize = formatString(messagesConfig.getString("commands.verified.size"));
commands.verifiedRemove = formatString(messagesConfig.getString("commands.verified.removed"));
commands.verifiedAdd = formatString(messagesConfig.getString("commands.verified.added"));
commands.verifiedAlready = formatString(messagesConfig.getString("commands.verified.already"));
commands.verifiedNotFound = formatString(messagesConfig.getString("commands.verified.ip-not-found"));
commands.verifiedBlocked = formatString(messagesConfig.getString("commands.verified.blocked"));
@@ -525,8 +526,7 @@ public final class SonarConfiguration {
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Commands {
private String incorrectCommandUsage;
private String incorrectIpAddress;
private String illegalIpAddress;
private String invalidIpAddress;
private String subCommandNoPerm;
private String playersOnly;
private String consoleOnly;
@@ -543,6 +543,8 @@ public final class SonarConfiguration {
private String blacklistSize;
private String verifiedRemove;
private String verifiedAdd;
private String verifiedAlready;
private String verifiedNotFound;
private String verifiedCleared;
private String verifiedSize;
@@ -310,13 +310,4 @@ public final class VerifiedPlayerController {
public boolean has(final @NotNull String inetAddress) {
return cache.containsKey(inetAddress);
}
/**
* @param inetAddress IP address
* @return Whether the local cache contains the IP
* @see #has(String)
*/
public boolean has(final @NotNull InetAddress inetAddress) {
return has(inetAddress.toString());
}
}
@@ -23,6 +23,7 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
@@ -32,6 +33,7 @@ import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
@@ -59,6 +61,20 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
return bukkitAudiences.player(uniqueId);
}
@Override
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
int count = 1;
for (final Player player : getPlugin().getServer().getOnlinePlayers()) {
// I don't know why, but Bukkit is *special*
if (Objects.isNull(player.getAddress())) continue;
// Check if the IP address of the player is equal to the IP trying to connect
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
// Increment count of duplicated accounts
if (++count >= limit) return true;
}
return false;
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -21,6 +21,7 @@ import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
@@ -30,10 +31,11 @@ import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -59,6 +61,19 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
return bungeeAudiences.player(uniqueId);
}
@Override
@SuppressWarnings("deprecation")
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
int count = 1;
for (final ProxiedPlayer player : getPlugin().getServer().getPlayers()) {
// Check if the IP address of the player is equal to the IP trying to connect
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
// Increment count of duplicated accounts
if (++count >= limit) return true;
}
return false;
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -99,9 +114,6 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
// Register traffic service
getPlugin().getServer().getScheduler().schedule(getPlugin(), CachedBandwidthStatistics::reset,
1L, 1L, TimeUnit.SECONDS);
@@ -44,7 +44,6 @@ import java.util.UUID;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
@@ -135,15 +134,13 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
@@ -172,8 +169,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
final String offlineUUIDString = "OfflinePlayer:" + loginRequest.getData();
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
@@ -186,8 +182,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
@@ -1,84 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.Kick;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.SonarBungee;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
@RequiredArgsConstructor
public final class FallbackLoginListener implements Listener {
private static final Field CHANNEL_FIELD;
static {
try {
CHANNEL_FIELD = InitialHandler.class.getDeclaredField("ch");
CHANNEL_FIELD.setAccessible(true);
} catch (NoSuchFieldException exception) {
throw new ReflectiveOperationException(exception);
}
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PreLoginEvent event) throws IllegalAccessException {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Don't do anything if the setting is disabled
if (maxOnlinePerIp <= 0) return;
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
.count()
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
final String serialized = GsonComponentSerializer.gson().serialize(component);
final PendingConnection connection = event.getConnection();
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
ProtocolVersion.fromId(connection.getVersion()),
new Kick(ComponentSerializer.deserialize(serialized)));
}
}
}
@@ -27,10 +27,14 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
@RequiredArgsConstructor
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
protected final Channel channel;
@@ -80,4 +84,30 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
// Simply close the channel if we encounter any errors.
ctx.close();
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded channel handler context
* @param packet Login packet sent by the client
*/
protected void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object packet,
final @NotNull String encoder,
final @NotNull String boss) {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
if (Sonar.get().hasTooManyAccounts(inetAddress, maxOnlinePerIp)) {
customDisconnect(channel, protocolVersion, FallbackPreparer.tooManyOnlinePerIP, encoder, boss);
return;
}
}
ctx.fireChannelRead(packet);
// Deject the channel since we don't need it anymore
deject(channel.pipeline());
}
}
@@ -64,6 +64,7 @@ public class FallbackPreparer {
public FallbackPacket reconnectedTooFast;
public FallbackPacket protocolBlacklisted;
public FallbackPacket invalidUsername;
public FallbackPacket tooManyOnlinePerIP;
// Vehicle
public FallbackPacket removeEntities;
@@ -137,6 +138,7 @@ public class FallbackPreparer {
protocolBlacklisted = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(), true);
reconnectedTooFast = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getTooFastReconnect(), true);
invalidUsername = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(), true);
tooManyOnlinePerIP = DisconnectPacket.create(Sonar.get().getConfig().getTooManyOnlinePerIp(), true);
// Precompute captcha answers
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
@@ -60,7 +60,7 @@ public final class BlacklistCommand extends Subcommand {
}
// Display a warning if the IP is verified but being added to the blacklist
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
.replace("%ip%", rawInetAddress));
}
@@ -23,8 +23,10 @@ import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
@@ -35,6 +37,7 @@ import java.util.concurrent.LinkedBlockingQueue;
arguments = {
@Argument("history"),
@Argument("remove"),
@Argument("add"),
@Argument("clear"),
@Argument("size")
}
@@ -107,6 +110,51 @@ public final class VerifiedCommand extends Subcommand {
break;
}
case "add": {
if (invocation.getRawArguments().length <= 3) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified add <IP address> <UUID/username>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
if (LOCK.contains(rawInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
return;
}
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
// Try to parse the UUID (from the username, if needed)
final String rawUUID = invocation.getRawArguments()[3];
final UUID uuid = rawUUID.length() == 36 ? UUID.fromString(rawUUID)
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
// Make sure the player is verified already
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString(), uuid)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAlready());
return;
}
// Lock the IP address
// Make sure we don't accidentally run 2 operations at the same time
LOCK.add(rawInetAddress);
// Add verified player to the database
final long timestamp = System.currentTimeMillis();
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(inetAddress.toString(), uuid, timestamp);
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAdd()
.replace("%ip%", rawInetAddress));
// Unlock the IP address
LOCK.remove(rawInetAddress);
break;
}
case "clear": {
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
@@ -39,8 +39,6 @@ commands:
incorrect-usage: '%prefix%<red>Benutzung: /sonar %usage%'
# Nachricht, die angezeigt wird, wenn jemand eine ungültige IP-Adresse angibt (Ungültiges Format)
invalid-ip-address: '%prefix%Die angegebene IP-Adresse scheint ungültig zu sein.'
# Nachricht, die angezeigt wird, wenn jemand eine illegale IP-Adresse angibt (Lokale IP)
illegal-ip-address: '%prefix%Die angegebene IP-Adresse scheint entweder eine lokale oder eine Schleifenrückführungs-IP zu sein.'
# Nachricht, die angezeigt wird, wenn die Konsole einen Befehl ausführt, der nur für Spieler ist
player-only: '%prefix%<red>Diesen Befehl kannst du nur als Spieler ausführen.'
# Nachricht, die angezeigt wird, wenn ein Spieler einen Befehl ausführt, der nur für die Konsole ist
@@ -123,6 +121,10 @@ commands:
size: '%prefix%Es sind derzeit %amount% einzigartige Spieler verifiziert.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht
removed: '%prefix%Erfolgreich %ip% unverifiziert.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse verifiziert macht
added: '%prefix%Erfolgreich %ip% verifiziert.'
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
already: '%prefix%The IP address and UUID you provided are already verified.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht, die nicht verifiziert ist
ip-not-found: '%prefix%Die angegebene IP-Adresse ist nicht verifiziert.'
# Nachricht, die angezeigt wird, wenn jemand versucht, die gleiche IP-Adresse zweimal zu unverifizieren (doppelte Operation)
@@ -38,9 +38,6 @@ commands:
incorrect-usage: '%prefix%<red>Usage: /sonar %usage%'
# Message that is shown when someone provides an invalid IP address (Invalid format)
invalid-ip-address: '%prefix%The IP address you provided seems to be invalid.'
# Message that is shown when someone provides an illegal IP address (Local IP)
illegal-ip-address: '%prefix%The IP address you provided seems to be either a local
or loopback IP.'
# Message that is shown when the console runs a command that is player-only
player-only: '%prefix%<red>You can only execute this command as a player.'
# Message that is shown when a player runs a command that is console-only
@@ -127,6 +124,10 @@ commands:
size: '%prefix%There are currently %amount% unique player(s) verified.'
# Message that is shown when someone un-verifies an IP address
removed: '%prefix%Successfully unverified %ip%.'
# Message that is shown when someone verifies an IP address
added: '%prefix%Successfully verified %ip%.'
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
already: '%prefix%The IP address and UUID you provided are already verified.'
# Message that is shown when someone un-verifies an IP address, but it is not verified
ip-not-found: '%prefix%The IP address you provided is not verified.'
# Message that is shown when someone tries un-verifying the same IP address twice (double operation)
@@ -38,8 +38,6 @@ commands:
incorrect-usage: '%prefix%<red>Применение: /sonar %usage%'
# Сообщение, которое отображается, когда кто-то предоставляет неверный формат IP-адреса
invalid-ip-address: '%prefix%Указанный вами IP-адрес недействителен'
# Сообщение, которое отображается, когда кто-то предоставляет локальный IP-адрес
illegal-ip-address: '%prefix%IP-адрес который вы указали является локальным.'
# Сообщение, которое отображается, когда консоль выполняет команду, предназначенную только для игроков
player-only: '%prefix%<red>Эта команда может быть выполнена только игроком.'
# Сообщение, которое отображается, когда игрок выполняет команду, доступную только для консоли
@@ -124,6 +122,10 @@ commands:
size: '%prefix%В настоящее время проверено %amount% уникальных игроков.'
# Сообщение, которое отображается при удалении IP-адреса из списка проверенных игроков
removed: '%prefix%Успешно удален %ip%.'
# Message that is shown when someone verifies an IP address
added: '%prefix%Successfully verified %ip%.'
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
already: '%prefix%The IP address and UUID you provided are already verified.'
# Сообщение, которое показывается когда кто-то удаляет IP из списка но его небыло в списке
ip-not-found: '%prefix%Указанный вами IP-адрес не в списке.'
# Сообщение, которое показывается когда выполняю сразу две операции (двойная операция)
@@ -38,8 +38,6 @@ commands:
incorrect-usage: '%prefix%<red>Kullanım: /sonar %usage%'
# Birisi geçersiz bir IP adresi sağladığında gösterilen mesaj (Geçersiz format)
invalid-ip-address: '%prefix%Sağladığınız IP adresi geçersiz görünüyor.'
# Birisi yasal olmayan bir IP adresi sağladığında gösterilen mesaj (Yerel IP)
illegal-ip-address: '%prefix%Sağladığınız IP adresi yerel veya döngüsel bir IP gibi görünüyor.'
# Konsol, sadece oyuncunun çalıştırabileceği bir komut çalıştırdığında gösterilen mesaj
player-only: '%prefix%<red>Bu komutu sadece bir oyuncu olarak çalıştırabilirsiniz.'
# Bir oyuncu, sadece konsolun çalıştırabileceği bir komutu çalıştırdığında gösterilen mesaj
@@ -115,6 +113,10 @@ commands:
size: '%prefix%Şu anda %amount% benzersiz oyuncu doğrulanmış.'
# Birisi bir IP adresini doğrulanmamış olarak işaretlediğinde gösterilen mesaj
removed: '%prefix%Başarıyla %ip% doğrulanmamış olarak işaretlendi.'
# Message that is shown when someone verifies an IP address
added: '%prefix%Successfully verified %ip%.'
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
already: '%prefix%The IP address and UUID you provided are already verified.'
# Birisi bir IP adresini doğrulanmamış olarak işaretlemeye çalıştığında ama doğrulanmamış olduğunda gösterilen mesaj
ip-not-found: '%prefix%Sağladığınız IP adresi doğrulanmış olarak işaretlenmemiş.'
# Birisi aynı IP adresini iki kez (çift işlem) doğrulanmamış olarak işaretlemeye çalıştığında gösterilen mesaj
@@ -18,6 +18,7 @@
package xyz.jonesdev.sonar.velocity;
import com.alessiodp.libby.VelocityLibraryManager;
import com.velocitypowered.api.proxy.Player;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import org.bstats.charts.SimplePie;
@@ -31,8 +32,9 @@ import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.velocity.fallback.FallbackLoginListener;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -58,6 +60,18 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
return getPlugin().getServer().getPlayer(uniqueId).orElse(null);
}
@Override
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
int count = 1;
for (final Player player : getPlugin().getServer().getAllPlayers()) {
// Check if the IP address of the player is equal to the IP trying to connect
if (!Objects.equals(player.getRemoteAddress().getAddress(), inetAddress)) continue;
// Increment count of duplicated accounts
if (++count >= limit) return true;
}
return false;
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -98,9 +112,6 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackLoginListener());
// Register traffic service
getPlugin().getServer().getScheduler().buildTask(getPlugin(), CachedBandwidthStatistics::reset)
.repeat(1L, TimeUnit.SECONDS)
@@ -40,7 +40,6 @@ import java.util.UUID;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
@@ -108,27 +107,25 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, blacklisted, MINECRAFT_DECODER, HANDLER);
customDisconnect(channel, protocolVersion, blacklisted, MINECRAFT_ENCODER, HANDLER);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
initialLogin(ctx, serverLogin, MINECRAFT_ENCODER, HANDLER);
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
initialLogin(ctx, serverLogin, MINECRAFT_ENCODER, HANDLER);
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyQueued, MINECRAFT_DECODER, HANDLER);
customDisconnect(channel, protocolVersion, alreadyQueued, MINECRAFT_ENCODER, HANDLER);
return;
}
@@ -136,14 +133,14 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(serverLogin.getUsername())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyVerifying, MINECRAFT_DECODER, HANDLER);
customDisconnect(channel, protocolVersion, alreadyVerifying, MINECRAFT_ENCODER, HANDLER);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted, MINECRAFT_DECODER, HANDLER);
customDisconnect(channel, protocolVersion, protocolBlacklisted, MINECRAFT_ENCODER, HANDLER);
return;
}
@@ -151,22 +148,20 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
final String offlineUUIDString = "OfflinePlayer:" + serverLogin.getUsername();
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
initialLogin(ctx, serverLogin, MINECRAFT_ENCODER, HANDLER);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
customDisconnect(channel, protocolVersion, reconnectedTooFast, MINECRAFT_DECODER, HANDLER);
customDisconnect(channel, protocolVersion, reconnectedTooFast, MINECRAFT_ENCODER, HANDLER);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
initialLogin(ctx, serverLogin, MINECRAFT_ENCODER, HANDLER);
return;
}
@@ -176,7 +171,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(serverLogin.getUsername()).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername, MINECRAFT_DECODER, HANDLER);
customDisconnect(channel, protocolVersion, invalidUsername, MINECRAFT_ENCODER, HANDLER);
return;
}
@@ -1,51 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.velocity.SonarVelocity;
import java.net.InetAddress;
import java.util.Objects;
public final class FallbackLoginListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PreLoginEvent event) {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Don't do anything if the setting is disabled
if (maxOnlinePerIp <= 0) return;
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
.filter(player -> Objects.equals(player.getRemoteAddress().getAddress(), inetAddress))
.count();
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
event.setResult(PreLoginEvent.PreLoginComponentResult.denied(Sonar.get().getConfig().getTooManyOnlinePerIp()));
}
}
}