Compare commits

...
15 Commits
Author SHA1 Message Date
Jones 23a472d3aa Release 2.0.18 (#202) 2024-02-18 16:26:17 +01:00
Jones 76f0f4abda Recode and optimize bandwidth counting (#201)
* feat: optimize bandwidth counting by recoding the channel handler

* fix: verification breaking new connections if disabled

* fix: add exception handlers to ensure that no error can reach the server
2024-02-18 16:16:11 +01:00
Jones 720e1cb1cb Fix incompatibility with Velocity 3.3.0 b360+ (#200) 2024-02-18 14:48:09 +01:00
Jones a98d9337c7 Fully recode injection system (#199)
* feat(all): fully recode injection technique

* fix: issues with proxy protocol enabled
2024-02-17 20:25:19 +01:00
Jones 5158111edf Implement automatic language system (#197)
* feat: [wip] better configuration system and better migration system

* feat: [wip] French translations for config.yml and webhook.yml

* fix(api): improve error handling if the config is invalid

* feat: implement better language detection system

* feat: allow user control over languages

* chore: use config logger in other config-related classes as well

* fix: only load MySQL driver if database is being used

* refactor: database url -> database host

* feat: [wip] add Russian translations for config & webhook (credits to @Nullmx/"nullme" on Discord)

* feat: [wip] add Czech translations for config & webhook (credits to @slosa)

* feat: [wip] add Georgian translations for config & webhook (credits to @Chumb3x)

* fix: missing Georgian translation in config/ka.yml

* feat: add Russian messages translation (credits to @Nullmx)

* feat: [wip] add Turkish translation for messages.yml (thanks to @AyazJenkins)

* fix: MySQL driver not being loaded correctly

* fix: some API calls not working because of caffeine

* feat: warn about translations not changing configured values
2024-02-17 16:50:17 +01:00
Jones 2ab829b43e Outline API methods and remove duplicate code (#193) 2024-02-10 20:45:43 +01:00
Jones 04bbfde6b6 Improve collision checks (#192)
* fix(api): edit misleading comment in configuration

* fix: make client brand check toggleable

* fix: only send abilities if needed

* feat: allow teleport ids to be negative

* fix(fallback): add a little randomization to spawn y position

* refactor: move map captcha packet handling to different method

* fix: actually fix 1.7 collisions properly

* feat: implement another simple and strict collision check

* feat: allow more control over the verification steps

* fix(api): hide verification failed logs during attacks if enabled
2024-02-10 19:47:42 +01:00
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
85 changed files with 3946 additions and 2794 deletions

No files matched your search

+2 -6
View File
@@ -41,12 +41,8 @@
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main anti-bot component
- Sonar queues new player logins to prevent spam login attacks
- Protective components only on BungeeCord:
- Sonar checks the handshake packets for legitimacy
- Sonar validates the login/server ping packets
- Sonar handles all server pings asynchronously
- The Varint decoder is updated to Velocity's
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
- Sonar checks the handshake packets for legitimacy
- Sonar makes sure some packets cannot be duplicated illegitimately
### Fallback
+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.18
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);
@@ -121,22 +121,23 @@ public final class AttackTracker implements JVMProfiler {
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
// Post webhook to Discord
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("%start-timestamp%", startTimestamp)
.replace("%end-timestamp%", endTimestamp)
.replace("%duration%", formattedDuration)
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
}));
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
.ifPresent(webhook -> webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("%start-timestamp%", startTimestamp)
.replace("%end-timestamp%", endTimestamp)
.replace("%duration%", formattedDuration)
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
}));
}
// Reset the attack status
@@ -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) {
@@ -0,0 +1,209 @@
/*
* Copyright (C) 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.api.config;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@Getter
enum Language {
SYSTEM("system"),
AB("Abkhazian"),
AA("Afar"),
AF("Afrikaans"),
SQ("Albanian"),
AM("Amharic"),
AR("Arabic"),
AN("Aragonese"),
HY("Armenian"),
AS("Assamese"),
AE("Avestan"),
AY("Aymara"),
AZ("Azerbaijani"),
BA("Bashkir"),
EU("Basque"),
BE("Belarusian"),
BN("Bengali"),
BH("Bihari"),
BI("Bislama"),
BS("Bosnian"),
BR("Breton"),
BG("Bulgarian"),
MY("Burmese"),
CA("Catalan"),
CH("Chamorro"),
CE("Chechen"),
ZH("Chinese"),
CU("Slavic"),
CV("Chuvash"),
KW("Cornish"),
CO("Corsican"),
HR("Croatian"),
CS("Czech"),
DA("Danish"),
DV("Maldivian"),
NL("Dutch"),
DZ("Dzongkha"),
EN("English"),
EO("Esperanto"),
ET("Estonian"),
FO("Faroese"),
FJ("Fijian"),
FI("Finnish"),
FR("French"),
GD("Gaelic"),
GL("Galician"),
KA("Georgian"),
DE("German"),
EL("Greek"),
GN("Guarani"),
GU("Gujarati"),
HT("Haitian Creole"),
HA("Hausa"),
HE("Hebrew"),
HZ("Herero"),
HI("Hindi"),
HO("Hiri Motu"),
HU("Hungarian"),
IS("Icelandic"),
IO("Ido"),
ID("Indonesian"),
IE("Interlingue"),
IU("Inuktitut"),
IK("Inupiaq"),
GA("Irish"),
IT("Italian"),
JA("Japanese"),
JV("Javanese"),
KL("Kalaallisut"),
KN("Kannada"),
KS("Kashmiri"),
KK("Kazakh"),
KM("Khmer"),
KI("Kikuyu"),
RW("Kinyarwanda"),
KY("Kirghiz"),
KV("Komi"),
KO("Korean"),
KJ("Kwanyama"),
KU("Kurdish"),
LO("Lao"),
LA("Latin"),
LV("Latvian"),
LI("Limburgish"),
LN("Lingala"),
LT("Lithuanian"),
LB("Luxembourgish"),
MK("Macedonian"),
MG("Malagasy"),
MS("Malay"),
ML("Malayalam"),
MT("Maltese"),
GV("Manx"),
MI("Maori"),
MR("Marathi"),
MH("Marshallese"),
MO("Moldavian"),
MN("Mongolian"),
NA("Nauru"),
NV("Navajo"),
ND("Ndebele, North"),
NR("Ndebele, South"),
NG("Ndonga"),
NE("Nepali"),
SE("Northern Sami"),
NO("Norwegian"),
NB("Norwegian Bokmal"),
NN("Norwegian Nynorsk"),
NY("Chewa"),
OC("Occitan"),
OR("Oriya"),
OM("Oromo"),
OS("Ossetian"),
PI("Pali"),
PA("Panjabi"),
FA("Persian"),
PL("Polish"),
PT("Portuguese"),
PS("Pushto"),
QU("Quechua"),
RM("Raeto-Romance"),
RO("Romanian"),
RN("Rundi"),
RU("Russian"),
SM("Samoan"),
SG("Sango"),
SA("Sanskrit"),
SC("Sardinian"),
SR("Serbian"),
SN("Shona"),
II("Sichuan Yi"),
SD("Sindhi"),
SI("Sinhala"),
SK("Slovak"),
SL("Slovenian"),
SO("Somali"),
ST("Sotho, Southern"),
ES("Spanish"),
SU("Sundanese"),
SW("Swahili"),
SS("Swati"),
SV("Swedish"),
TL("Tagalog"),
TY("Tahitian"),
TG("Tajik"),
TA("Tamil"),
TT("Tatar"),
TE("Telugu"),
TH("Thai"),
BO("Tibetan"),
TI("Tigrinya"),
TO("Tonga"),
TS("Tsonga"),
TN("Tswana"),
TR("Turkish"),
TK("Turkmen"),
TW("Twi"),
UG("Uighur"),
UK("Ukrainian"),
UR("Urdu"),
UZ("Uzbek"),
VI("Vietnamese"),
VO("Volapuk"),
WA("Walloon"),
CY("Welsh"),
FY("Western Frisian"),
WO("Wolof"),
XH("Xhosa"),
YI("Yiddish"),
YO("Yoruba"),
ZA("Zhuang"),
ZU("Zulu");
private final String name;
private final String code;
Language(final String name) {
this.name = name;
this.code = name().toLowerCase();
}
public static Language fromCode(final @NotNull String code) {
return Language.valueOf(code.toUpperCase());
}
}
@@ -19,64 +19,69 @@ package xyz.jonesdev.sonar.api.config;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
import org.simpleyaml.configuration.MemorySection;
import org.simpleyaml.configuration.file.YamlFile;
import xyz.jonesdev.sonar.api.Sonar;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
@Getter
public final class SimpleYamlConfig {
private final File file;
private final YamlFile yaml;
private static final List<String> HEADER = Arrays.asList(
"",
String.format("Running Sonar version %s on %s",
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
"Need help or have questions? https://jonesdev.xyz/discord",
"https://github.com/jonesdevelopment/sonar",
""
);
public SimpleYamlConfig(final File dataFolder, final String fileName) {
this(new File(dataFolder, fileName + ".yml"), dataFolder);
}
private SimpleYamlConfig(final File file, final @NotNull File folder) {
if (!folder.exists() && !folder.mkdir()) {
throw new IllegalStateException("Could not create folder?!");
}
public SimpleYamlConfig(final @NotNull File file) {
this.file = file;
this.yaml = new YamlFile(file.getPath());
}
public void save() {
try {
yaml.save();
} catch (IOException exception) {
exception.printStackTrace(System.err);
private void replaceConfigFile(final @NotNull URL defaultData) throws Exception {
try (final InputStream inputStream = defaultData.openStream()) {
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
public void load() throws Exception {
// https://github.com/jonesdevelopment/sonar/issues/26
// Only load the configuration if the file already exists
if (yaml.exists()) {
yaml.loadWithComments();
} else {
yaml.createOrLoadWithComments();
public void load(final @NotNull URL defaultData) throws Exception {
final boolean exists = yaml.exists();
// Copy the configuration from the translations
if (!exists) {
replaceConfigFile(defaultData);
}
// Always load the comment format and options
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
// Load the configuration
yaml.loadWithComments();
// I hate my life... but this works... somehow
if (exists) {
// First, we store all values from the current (old) config
final Map<String, Object> values = yaml.getValues(true);
// Then, we replace the old config file with the default one
replaceConfigFile(defaultData);
// Re-load the configuration
yaml.loadWithComments();
// Loop through the new values from the new configuration
yaml.getValues(true).forEach((path, v) -> {
final Object value = values.get(path);
// Make sure we actually need to update the value
if (value != null && !value.equals(v)
&& !(v instanceof MemorySection)
&& !(value instanceof MemorySection)) {
// Set the old values to the new values
yaml.set(path, value);
}
});
// Save the yaml file to ensure we keep the original values
yaml.save();
}
}
public void set(final String path, final Object v) {
@@ -88,33 +93,81 @@ public final class SimpleYamlConfig {
return yaml.getInt(path, def);
}
public int getInt(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return 0;
}
return yaml.getInt(path);
}
public boolean getBoolean(final String path, final boolean def) {
yaml.addDefault(path, def);
return yaml.getBoolean(path, def);
}
public boolean getBoolean(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return false;
}
return yaml.getBoolean(path);
}
public String getString(final String path, final String def) {
final Object object = getObject(path, def);
if (object instanceof String) {
return (String) object;
}
Sonar.get().getLogger().info("[config] Migrated {} to {}", path, def);
LOGGER.info("[config] Migrated {} to {}", path, def);
set(path, def);
return def;
}
public @NotNull String getString(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return "";
}
final Object object = getObject(path);
if (object instanceof String) {
return (String) object;
}
throw new IllegalStateException("Invalid entry " + path);
}
public Object getObject(final String path, final Object def) {
yaml.addDefault(path, def);
return yaml.get(path, def);
}
public Object getObject(final String path) {
return yaml.get(path);
}
public List<String> getStringList(final String path, final List<String> def) {
yaml.addDefault(path, def);
return yaml.getStringList(path);
}
public List<String> getStringList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getStringList(path);
}
public List<Integer> getIntList(final String path, final List<Integer> def) {
yaml.addDefault(path, def);
return yaml.getIntegerList(path);
}
public List<Integer> getIntList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getIntegerList(path);
}
}
File diff suppressed because it is too large. Load diff
@@ -29,6 +29,7 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.lang.reflect.Method;
@@ -45,6 +46,24 @@ public final class VerifiedPlayerController {
private static final Map<String, Collection<UUID>> MAP = new ConcurrentHashMap<>();
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[database] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[database] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[database] " + message, args);
}
};
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@@ -57,7 +76,7 @@ public final class VerifiedPlayerController {
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
LOGGER.warn("Configure a database to save verified players.");
return;
}
@@ -85,11 +104,11 @@ public final class VerifiedPlayerController {
try {
dao.queryForAll().forEach(this::_add);
} catch (SQLException exception) {
Sonar.get().getLogger().error("Could not cache entries: {}", exception);
LOGGER.error("Could not cache entries: {}", exception);
}
});
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Error setting up database: {}", throwable);
LOGGER.error("Error setting up database: {}", throwable);
throwable.printStackTrace(System.err);
}
}
@@ -103,7 +122,7 @@ public final class VerifiedPlayerController {
// Prepare the JDBC connection string
// TODO: automatically build best connection string for database type
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
database.getType().name().toLowerCase(), database.getHost(), database.getPort(), database.getName());
// Use reflection to uncover the driver class
final Class<?> driverClass = Class.forName(database.getType().getDriverClassName());
@@ -151,11 +170,11 @@ public final class VerifiedPlayerController {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to clear old entries: {}", exception);
LOGGER.error("Error trying to clear old entries: {}", exception);
}
}
@@ -191,7 +210,7 @@ public final class VerifiedPlayerController {
}
}
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to remove entry: {}", exception);
LOGGER.error("Error trying to remove entry: {}", exception);
}
});
}
@@ -229,7 +248,7 @@ public final class VerifiedPlayerController {
try {
dao.create(player);
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to add entry: {}", exception);
LOGGER.error("Error trying to add entry: {}", exception);
}
});
}
@@ -273,7 +292,7 @@ public final class VerifiedPlayerController {
dao.deleteBuilder().delete();
}
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to clear entries: {}", exception);
LOGGER.error("Error trying to clear entries: {}", exception);
}
}
@@ -31,5 +31,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
private final FallbackUser<?> user;
private final FallbackUser user;
}
@@ -30,11 +30,11 @@ import java.util.Optional;
@ToString
@EqualsAndHashCode
public final class UserVerifyFailedEvent implements SonarEvent {
private final FallbackUser<?> user;
private final FallbackUser user;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<String> reason;
public UserVerifyFailedEvent(final FallbackUser<?> user, final @Nullable String reason) {
public UserVerifyFailedEvent(final FallbackUser user, final @Nullable String reason) {
this.user = user;
this.reason = Optional.ofNullable(reason);
}
@@ -30,5 +30,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
private final FallbackUser<?> user;
private final FallbackUser user;
}
@@ -33,6 +33,6 @@ import java.util.UUID;
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser<?> user;
private final FallbackUser user;
private final long timeTakenToVerify;
}
@@ -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;
@@ -21,6 +21,5 @@ public interface FallbackPipelines {
String FALLBACK_HANDLER = "sonar-fallback-handler";
String FALLBACK_PACKET_ENCODER = "sonar-fallback-encoder";
String FALLBACK_PACKET_DECODER = "sonar-fallback-decoder";
String FALLBACK_IN_TRAFFIC = "sonar-traffic-in";
String FALLBACK_OUT_TRAFFIC = "sonar-traffic-out";
String FALLBACK_BANDWIDTH = "sonar-fallback-bandwidth";
}
@@ -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,13 @@
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,37 +31,17 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
public interface FallbackUser<T> {
@NotNull Fallback getFallback();
/**
* @return Per-platform connection object used internally
*/
@NotNull T getConnection();
import java.time.Duration;
import java.util.Objects;
import java.util.UUID;
public interface FallbackUser {
@NotNull Channel getChannel();
@NotNull ChannelPipeline getPipeline();
@NotNull InetAddress getInetAddress();
@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.
@@ -72,6 +50,20 @@ public interface FallbackUser<T> {
*/
void disconnect(final @NotNull Component reason);
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param uuid UUID of the player
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
* @param boss Name of the main pipeline
*/
void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String boss);
/**
* Sends a packet/message to the player
*
@@ -97,7 +89,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
@@ -112,10 +136,15 @@ public interface FallbackUser<T> {
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (reason != null) {
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
// Only log the failed message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
}
}
}
@@ -124,21 +153,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());
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
Sonar.get().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();
}
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.fallback.traffic;
package xyz.jonesdev.sonar.api.statistics;
import lombok.Getter;
import lombok.Setter;
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
* This counts all incoming and outgoing traffic.
*/
@Getter
public enum TrafficCounter implements JVMProfiler {
public enum Bandwidth implements JVMProfiler {
INCOMING,
OUTGOING;
@@ -48,7 +48,7 @@ public enum TrafficCounter implements JVMProfiler {
* This method is called every second.
*/
public static synchronized void reset() {
for (final TrafficCounter value : values()) {
for (final Bandwidth value : values()) {
value.cacheAndReset();
}
}
@@ -17,12 +17,29 @@
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 org.jetbrains.annotations.ApiStatus;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
@SuppressWarnings("unused")
public interface Counters {
ExpiringCache<Long> LOGINS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
ExpiringCache<Long> CONNECTIONS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
@ApiStatus.Internal
Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
@ApiStatus.Internal
Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
static long getConnectionsPerSecond() {
return CONNECTIONS_PER_SECOND.estimatedSize();
}
static long getLoginsPerSecond() {
return LOGINS_PER_SECOND.estimatedSize();
}
}
@@ -31,8 +31,8 @@ import java.util.Collection;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
@Getter
public final class Notification implements Observable, Counters {
@@ -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()))
@@ -34,8 +34,8 @@ import java.util.Collection;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
@Getter
public final class Verbose implements Observable, JVMProfiler, Counters {
@@ -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
@@ -24,8 +24,8 @@ import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
@@ -86,7 +86,7 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
// Register traffic service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
TrafficCounter::reset, 20L, 20L);
Bandwidth::reset, 20L, 20L);
// Register queue service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
+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
@@ -24,13 +24,12 @@ import org.bstats.bungeecord.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.concurrent.TimeUnit;
@@ -83,16 +82,13 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
// Inject base into ProtocolUtils
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
// Register traffic service
getPlugin().getServer().getScheduler().schedule(getPlugin(), TrafficCounter::reset,
getPlugin().getServer().getScheduler().schedule(getPlugin(), Bandwidth::reset,
1L, 1L, TimeUnit.SECONDS);
// Register queue service
@@ -102,5 +98,8 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Register verbose service
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
200L, 200L, TimeUnit.MILLISECONDS);
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject();
}
}
@@ -0,0 +1,269 @@
/*
* 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 com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
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.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
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.closeWith;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private final Channel channel;
private @Nullable String username;
private ProtocolVersion protocolVersion;
private InetAddress inetAddress;
private static final Fallback FALLBACK = Sonar.get().getFallback();
private static final Cache<Component, Kick> CACHED_KICK_PACKETS = Caffeine.newBuilder().build();
private static final Field CHANNEL_WRAPPER_FIELD;
static {
try {
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
CHANNEL_WRAPPER_FIELD.setAccessible(true);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
public static @NotNull Kick getCachedKickPacket(final Component component) {
Kick packet = CACHED_KICK_PACKETS.asMap().get(component);
if (packet == null) {
final String serialized = JSONComponentSerializer.json().serialize(component);
final BaseComponent baseComponent = ComponentSerializer.deserialize(serialized);
packet = new Kick(baseComponent);
CACHED_KICK_PACKETS.put(component, packet);
}
return packet;
}
@Override
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// The player can disconnect without sending the login packet first
if (username != null) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
}
// The player cannot be in the queue if the IP address is invalid
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
if (wrappedPacket != null) {
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
handleHandshake((Handshake) wrappedPacket);
}
// Intercept any server login packet by the client
if (wrappedPacket instanceof LoginRequest) {
handleLogin(ctx, (LoginRequest) wrappedPacket, msg);
return;
}
}
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
// We can override the default exceptionCaught method since this handler
// will run before the MinecraftConnection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Sonar sometimes uses exceptions to quickly close
// the channel and interrupt any other ongoing process.
// Simply close the channel if we encounter any errors.
ctx.close();
}
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Check if the hostname is invalid
if (handshake.getHost().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
// Hook the traffic listener
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull LoginRequest loginRequest,
final @NotNull Object wrappedMessage) throws Exception {
// Check if the player has already sent a login packet
if (username != null) {
throw new CorruptedFrameException("Already logged on");
}
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Store the username
username = loginRequest.getData();
// Make sure to use the potentially modified, real IP
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(wrappedMessage);
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(wrappedMessage);
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
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())) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
return;
}
// Make sure we actually have to verify the player
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);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
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(wrappedMessage);
return;
}
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(loginRequest.getData()).matches()) {
closeWith(channel, protocolVersion,
Sonar.get().getConfig().getVerification().getInvalidUsername());
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
// Let the verification handler take over the channel
user.hijack(loginRequest.getData(), offlineUUID, PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
}));
}
}
@@ -0,0 +1,56 @@
/*
* 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 io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import java.lang.reflect.Method;
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
@RequiredArgsConstructor
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
private static final Method INIT_CHANNEL_METHOD;
static {
try {
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
INIT_CHANNEL_METHOD.setAccessible(true);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
private final ChannelInitializer<Channel> originalChannelInitializer;
@Override
protected void initChannel(final Channel channel) throws Exception {
// Invoke the original method
try {
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
}
}
@@ -1,409 +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 io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.DownstreamBridge;
import net.md_5.bungee.connection.UpstreamBridge;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketHandler;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.StatusRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
public final class FallbackHandlerBoss extends HandlerBoss {
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
private static final @NotNull BungeeCord BUNGEE = BungeeCord.getInstance();
private static final Field CHANNEL_WRAPPER_FIELD;
static {
try {
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
CHANNEL_WRAPPER_FIELD.setAccessible(true);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
private ChannelWrapper channelWrapper;
private PacketHandler handler;
private @Nullable FallbackUserWrapper user;
@Getter
private ProtocolVersion protocolVersion;
private boolean receivedLoginPacket;
private boolean receivedStatusPacket;
@Override
public void setHandler(final @NotNull PacketHandler handler) {
this.handler = handler;
super.setHandler(handler);
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
super.channelActive(ctx);
// 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());
}
}
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
if (msg instanceof HAProxyMessage || msg instanceof PacketWrapper) {
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
// Handle Handshake packets
if (packetWrapper.packet instanceof Handshake) {
final Handshake handshake = (Handshake) packetWrapper.packet;
// Check if the host is invalid
if (handshake.getHost().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Set protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
}
// Handle StatusRequest packets
if (packetWrapper.packet instanceof StatusRequest) {
// Fix status packet spam exploit
if (receivedStatusPacket) {
throw new CorruptedFrameException("Duplicate status packet");
}
receivedStatusPacket = true;
}
// Handle LoginRequest packets
if (packetWrapper.packet instanceof LoginRequest) {
final LoginRequest loginRequest = (LoginRequest) packetWrapper.packet;
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime());
// Fix login packet spam exploit
if (receivedLoginPacket || user != null) {
throw new CorruptedFrameException("Duplicate login packet");
}
receivedLoginPacket = true;
// Cache protocol version so other handlers don't throw NPEs
final Channel channel = channelWrapper.getHandle();
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
try {
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Use the ChannelWrapper's remote address to automatically handle proxy protocol
final InetAddress inetAddress = ((InetSocketAddress) channelWrapper.getRemoteAddress()).getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
// Check if the verification is enabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
super.channelRead(ctx, msg);
return;
}
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
super.channelRead(ctx, msg);
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())) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
return;
}
final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData())
.getBytes(StandardCharsets.UTF_8));
// Check if the player is already verified
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
super.channelRead(ctx, msg);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
super.channelRead(ctx, msg);
return;
}
// Create wrapped Fallback user
user = new FallbackUserWrapper(
FALLBACK, this, channel, channel.pipeline(),
inetAddress, protocolVersion);
// Perform default BungeeCord checks
if (BUNGEE.config.isEnforceSecureProfile()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
if (publicKey == null) {
closeWith(getKickPacket(Component.text(BUNGEE.getTranslation("secure_profile_required"))));
return;
}
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
closeWith(getKickPacket(BUNGEE.getTranslation("secure_profile_expired")));
return;
}
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
&& !EncryptionUtil.check(publicKey, null)) {
closeWith(getKickPacket(BUNGEE.getTranslation("secure_profile_invalid")));
return;
}
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// Disconnect if the protocol version could not be resolved
if (user.getProtocolVersion().isUnknown()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
return;
}
// Check if the player is already connected to the proxy but still tries to verify
final int limit = BUNGEE.config.getPlayerLimit();
if (limit > 0 && BUNGEE.getOnlineCount() >= limit) {
closeWith(getKickPacket(BUNGEE.getTranslation("proxy_full")));
return;
} else if (!BUNGEE.getConfig().isOnlineMode() && BUNGEE.getPlayer(loginRequest.getData()) != null) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", loginRequest.getData())
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
// Mark the player as connected → verifying players
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (user.getPipeline().get(PACKET_ENCODER) == null
|| user.getPipeline().get(PACKET_DECODER) == null) {
channelWrapper.close();
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
user.write(new LoginSuccess(loginRequest.getData(), uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
return;
}
}
super.channelRead(ctx, msg);
}
}
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) {
if (ctx.channel().isActive()) {
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
try {
handler.exception(throwable);
} catch (Exception exception) {
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
}
}
channelWrapper.markClosed();
ctx.close();
}
}
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
public static @NotNull Kick getKickPacket(final @NotNull String reason) {
final Component component = Component.text(reason);
return getKickPacket(component);
}
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
if (cachedKickPacket == null) {
final String serialized = JSONComponentSerializer.json().serialize(component);
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
}
return cachedKickPacket;
}
public void closeWith(final Object msg) {
closeWith(channelWrapper, protocolVersion, msg);
}
// Mostly taken from Velocity
public static void closeWith(final @NotNull ChannelWrapper channelWrapper,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channelWrapper.getHandle().isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
channelWrapper.getHandle().eventLoop().execute(() -> {
channelWrapper.getHandle().config().setAutoRead(false);
channelWrapper.getHandle().eventLoop().schedule(() -> {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.injection;
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
@@ -27,20 +27,27 @@ import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import java.lang.reflect.Field;
@UtilityClass
public class BaseInjectionHelper {
public void inject(final ChannelInitializer<Channel> childHandler) {
public class FallbackInjectionHelper {
public void inject() {
try {
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
childField.setAccessible(true);
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
// Get the base object (the object containing the field)
final Object base = unsafe.staticFieldBase(childField);
// Get the offset of the static field within its class
final long offset = unsafe.staticFieldOffset(childField);
unsafe.putObject(base, offset, childHandler);
// Replace the original channel initializer
unsafe.putObject(base, offset, fallbackInitializer);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
@@ -36,8 +36,11 @@ import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.bungee.fallback.FallbackChannelHandler.getCachedKickPacket;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
@RequiredArgsConstructor
public final class FallbackListener implements Listener {
public final class FallbackLoginListener implements Listener {
private static final Field CHANNEL_FIELD;
@@ -53,25 +56,25 @@ public final class FallbackListener implements Listener {
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull LoginEvent event) throws IllegalAccessException {
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
// 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;
if (maxOnlinePerIp > 0) {
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 */;
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
final PendingConnection connection = event.getConnection();
FallbackHandlerBoss.closeWith((ChannelWrapper) CHANNEL_FIELD.get(connection),
ProtocolVersion.fromId(connection.getVersion()), FallbackHandlerBoss.getKickPacket(component));
}
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 PendingConnection connection = event.getConnection();
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
ProtocolVersion.fromId(connection.getVersion()), getCachedKickPacket(component));
}
}
}
@@ -1,53 +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 io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
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.packets.play.Disconnect;
import java.net.InetAddress;
@Getter
@RequiredArgsConstructor
public final class FallbackUserWrapper implements FallbackUser<FallbackHandlerBoss> {
private final Fallback fallback;
private final FallbackHandlerBoss connection;
private final Channel channel;
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
/**
* Disconnect the player during/after verification
* using our custom {@link Disconnect} packet.
*
* @param reason Disconnect message component
*/
@Override
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
}
@@ -1,80 +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.injection;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
static {
try {
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
frameEncoder.setAccessible(true);
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
// Taken from BungeeCord
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L162
@Override
protected void initChannel(final @NotNull Channel channel) throws Exception {
try {
channel.config().setOption(ChannelOption.IP_TOS, 0x18);
// Use TCPNoDelay
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
} catch (ChannelException exception) {
// IP_TOS is not supported (Windows XP / Windows Server 2003)
}
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
}
}
@@ -1,90 +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.injection;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.event.ClientConnectEvent;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.*;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
import java.lang.reflect.Field;
import java.net.SocketAddress;
import static net.md_5.bungee.netty.PipelineUtils.*;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
private static final KickStringWriter LEGACY_KICK;
static {
try {
final Field kickField = PipelineUtils.class.getDeclaredField("legacyKicker");
kickField.setAccessible(true);
LEGACY_KICK = (KickStringWriter) kickField.get(null);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
// Taken from BungeeCord
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
@Override
protected void initChannel(final @NotNull Channel channel) throws Exception {
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
: channel.remoteAddress();
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
channel.close();
return;
}
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
if (BungeeCord.getInstance().getPluginManager().callEvent(new ClientConnectEvent(remoteAddress, listener)).isCancelled()) {
channel.close();
return;
}
BASE.initChannel(channel);
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
Protocol.HANDSHAKE, true, BUNGEE.getProtocolVersion()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
Protocol.HANDSHAKE, true, BUNGEE.getProtocolVersion()));
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new InitialHandler(BUNGEE, listener));
if (listener.isProxyProtocol()) {
channel.pipeline().addFirst(new HAProxyMessageDecoder());
}
}
}
@@ -1,92 +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.varint;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import java.util.List;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final @NotNull ChannelHandlerContext ctx,
final ByteBuf byteBuf,
final List<Object> out) throws Exception {
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
// MessageToMessageDecoder always do release after decoding
// https://github.com/jonesdevelopment/sonar-legacy/pull/5
return;
}
final VarintByteDecoder reader = new VarintByteDecoder();
final int end = byteBuf.forEachByte(reader);
if (end == -1) {
// This is probably a good sign that the buffer was too short or empty
// since the ByteBuf cannot hold a proper VarInt.
if (reader.getResult() == VarintByteDecoder.DecoderResult.RUN_OF_ZEROES) {
byteBuf.clear();
}
return;
}
switch (reader.getResult()) {
case RUN_OF_ZEROES: {
// this will return to the point where the next varInt starts
byteBuf.readerIndex(end);
break;
}
case SUCCESS: {
final int readVarInt = reader.getReadVarInt();
if (readVarInt < 0) {
byteBuf.clear();
throw new CorruptedFrameException("Result: " + reader);
} else if (readVarInt == 0) {
// Actually, we don't want to throw an Exception if the packet is empty.
// The check would also false flag a lot of legit players since packets
// in 1.7 could sometimes be empty.
byteBuf.readerIndex(end + 1);
return;
}
final int bytesRead = reader.getBytesRead();
final int minimumRead = bytesRead + readVarInt;
if (byteBuf.isReadable(minimumRead)) {
out.add(byteBuf.retainedSlice(end + 1, readVarInt));
byteBuf.skipBytes(minimumRead);
}
break;
}
default: {
byteBuf.clear();
throw new CorruptedFrameException("Result: " + reader);
}
}
}
}
@@ -1,63 +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.varint;
import io.netty.util.ByteProcessor;
import lombok.Getter;
import lombok.ToString;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
@Getter
@ToString
public final class VarintByteDecoder implements ByteProcessor {
private DecoderResult result = DecoderResult.TOO_SMALL;
private int readVarInt, bytesRead;
@Override
public boolean process(byte b) throws Exception {
if (b == 0 && bytesRead == 0) {
result = DecoderResult.RUN_OF_ZEROES;
return true;
}
if (result == DecoderResult.RUN_OF_ZEROES) {
return false;
}
readVarInt |= (b & 0x7F) << bytesRead++ * 7;
if (bytesRead > 3) {
result = DecoderResult.TOO_BIG;
return false;
}
if ((b & 0x80) != 128) {
result = DecoderResult.SUCCESS;
return false;
}
return true;
}
public enum DecoderResult {
SUCCESS,
TOO_SMALL,
TOO_BIG,
RUN_OF_ZEROES
}
}
+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
@@ -29,7 +29,6 @@ class LibraryLoader {
libraryManager.addJitPack();
libraryManager.loadLibraries(
// Simple-YAML
// TODO: use a different library for config management?
Library.builder()
.groupId("com{}github{}Carleslc{}Simple-YAML")
.artifactId("Simple-Yaml")
@@ -43,6 +42,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")
@@ -50,7 +56,7 @@ class LibraryLoader {
.version("6.1")
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
.build(),
// MySQL
// MySQL driver
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
@@ -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
@@ -119,19 +116,37 @@ public abstract class SonarBootstrap<T> implements Sonar {
getConfig().load();
// 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"
if (verifiedPlayerController != null
&& verifiedPlayerController.getCachedDatabaseType() != getConfig().getDatabase().getType()) {
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();
@@ -15,30 +15,47 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.traffic;
package xyz.jonesdev.sonar.common.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
@ChannelHandler.Sharable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class OutgoingBandwidthEncoder extends ChannelOutboundHandlerAdapter {
public static final OutgoingBandwidthEncoder INSTANCE = new OutgoingBandwidthEncoder();
public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
public static final ChannelHandler INSTANCE = new FallbackBandwidthHandler();
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
// We can only get the size of a message if it's a ByteBuf
if (msg instanceof ByteBuf) {
// Increment the incoming traffic by the amount of readable bytes
INCOMING.increment(((ByteBuf) msg).readableBytes());
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
@Override
public void write(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg,
final @NotNull ChannelPromise promise) throws Exception {
final ChannelPromise promise) throws Exception {
// We can only get the size of a message if it's a ByteBuf
if (msg instanceof ByteBuf) {
TrafficCounter.OUTGOING.increment(((ByteBuf) msg).readableBytes());
// Increment the outgoing traffic by the amount of readable bytes
OUTGOING.increment(((ByteBuf) msg).readableBytes());
}
// Make sure to let the server handle the rest
ctx.write(msg, promise);
}
}
@@ -1,44 +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.common.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import java.net.InetAddress;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
private final @NotNull String username;
private final @NotNull InetAddress inetAddress;
@Override
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
// Make sure other handlers also know we've disconnected
ctx.fireChannelUnregistered();
}
}
@@ -0,0 +1,162 @@
/*
* 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.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
@Getter
@ToString(of = {"protocolVersion", "inetAddress"})
public final class FallbackUserWrapper implements FallbackUser {
private final Channel channel;
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion) {
this.channel = channel;
this.pipeline = channel.pipeline();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
}
/**
* Disconnect the player during/after verification
* using our custom {@link Disconnect} packet.
*
* @param reason Disconnect message component
*/
@Override
public void disconnect(final @NotNull Component reason) {
closeWith(channel, protocolVersion, Disconnect.create(reason));
}
/**
* @param channel Channel to close
* @param protocolVersion Protocol version
* @param msg Packet to close the channel with
*/
public static void closeWith(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channel.isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param uuid UUID of the player
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
*/
@Override
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String boss) {
// This rarely happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (pipeline.get(encoder) == null || pipeline.get(decoder) == null) {
channel.close();
return;
}
// Remove main pipeline to completely take over the channel
if (pipeline.get(boss) != null) {
pipeline.remove(boss);
}
// The player has joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", username)
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(protocolVersion.getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
// Mark the player as connected → verifying players
Sonar.get().getFallback().getConnected().put(username, inetAddress);
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
// Send LoginSuccess packet to make the client think they are joining the server
write(new LoginSuccess(username, uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
}
}
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@@ -43,6 +44,7 @@ import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@@ -50,10 +52,11 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVerificationHandler implements FallbackPacketListener {
private static final Random RANDOM = new Random();
private static final Fallback FALLBACK = Sonar.get().getFallback();
// General
private final SystemTimer login = new SystemTimer();
private final @NotNull FallbackUser<?> user;
private final @NotNull FallbackUser user;
private final String username;
private final UUID playerUuid;
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
@@ -62,7 +65,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private short expectedTransactionId;
private int expectedKeepAliveId, expectedTeleportId = -1;
private int tick, totalReceivedPackets, ignoredMovementTicks;
private double posX, posY, posZ, lastY;
private double posX, posY, posZ, lastY, spawnYPosition;
private boolean resolvedClientBrand, resolvedClientSettings;
private boolean listenForMovements;
@@ -87,7 +90,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
SUCCESS
}
public FallbackVerificationHandler(final @NotNull FallbackUser<?> user,
public FallbackVerificationHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID playerUuid) {
this.user = user;
@@ -121,7 +124,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
encoder.updateRegistry(registry);
} else {
// Something went wrong - the decoder should not be null
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
FALLBACK.getLogger().warn("Necessary pipelines for {} not found", username);
// Close the channel to prevent bypasses or other potential exploits
user.getChannel().close();
}
@@ -172,13 +175,17 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
// Make sure the player is unable to fly (the player is in spectator mode)
user.delayedWrite(DEFAULT_ABILITIES);
// Make sure the player is unable to fly (e.g. if the player is in creative mode)
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
user.delayedWrite(DEFAULT_ABILITIES);
}
// Generate the current teleport ID
expectedTeleportId = RANDOM.nextInt(Short.MAX_VALUE);
expectedTeleportId = RANDOM.nextInt();
// Add a little randomization to the spawn y coordinate
spawnYPosition = dynamicSpawnYPosition + RANDOM.nextDouble(0.4);
// Teleport the player to the spawn position
user.delayedWrite(new PositionLook(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
SPAWN_X_POSITION, spawnYPosition, SPAWN_Z_POSITION,
0f, -90f, expectedTeleportId, false));
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
@@ -187,6 +194,13 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void sendChunkData() {
// If we don't have gravity and captcha enabled, simply finish verification
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
&& !FALLBACK.shouldDoMapCaptcha()) {
// Save some work by finishing before sending even more packets
finish();
return;
}
// Set the state to POSITION to avoid false positives
// and go on with the flow of the verification.
state = State.POSITION;
@@ -212,14 +226,14 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?> user,
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
private static boolean validateClientBrand(final @NotNull FallbackUser<?> user, final ByteBuf content) {
private static boolean validateClientBrand(final @NotNull FallbackUser user, final ByteBuf content) {
// We have to catch every DecoderException, so we can fail and punish
// the player instead of only disconnecting them due to an exception.
try {
@@ -227,11 +241,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// already performs these checks by default.
final String read = ProtocolUtil.readBrandMessage(content);
// Check if the decoded client brand string is too long
if (read.length() > Sonar.get().getConfig().getVerification().getMaxBrandLength()) {
if (read.length() > Sonar.get().getConfig().getVerification().getBrand().getMaxLength()) {
return false;
}
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidBrandRegex();
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
return !read.equals("Vanilla") // The normal brand is always lowercase
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
} catch (DecoderException exception) {
@@ -242,57 +256,60 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
}
private void handlePacketDuringCaptcha(final @NotNull FallbackPacket packet) {
// Check if the player took too long to enter the captcha
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
// Handle incoming chat messages
if (packet instanceof Chat) {
final Chat chat = (Chat) packet;
Objects.requireNonNull(captcha);
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
return;
}
// Captcha is correct
finish();
return;
}
// Every second
if (actionBar.elapsed(1000L)) {
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
// Only send action bar if the message is actually supposed to be sent
if (!actionBarMessage.isEmpty()) {
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
user.write(new Chat(MiniMessage.miniMessage().deserialize(
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
// Make sure to reset the timer
actionBar.reset();
}
}
// Every 10 seconds
if (keepAlive.elapsed(10_000L)) {
// Send the message again to remind the player
user.delayedWrite(enterCodeMessage);
// Send a KeepAlive packet to prevent timeout
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
// Send both packets in one flush
user.getChannel().flush();
// Make sure to reset the timer
keepAlive.reset();
}
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified, drop all other packets
if (state == State.SUCCESS) return;
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
// We are expecting a captcha code in chat, drop all other packets
if (state == State.MAP_CAPTCHA) {
// Check if the player took too long to enter the captcha
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
// Handle incoming chat messages
if (packet instanceof Chat) {
final Chat chat = (Chat) packet;
Objects.requireNonNull(captcha);
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
return;
}
// Captcha is correct
finish();
return;
}
// Every second
if (actionBar.elapsed(1000L)) {
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
// Only send action bar if the message is actually supposed to be sent
if (!actionBarMessage.isEmpty()) {
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
user.write(new Chat(MiniMessage.miniMessage().deserialize(
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
// Make sure to reset the timer
actionBar.reset();
}
}
// Every about 10 seconds
if (keepAlive.elapsed(timeout)) {
// Send the message again to remind the player
user.delayedWrite(enterCodeMessage);
// Send a KeepAlive packet to prevent timeout
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
// Send both packets in one flush
user.getChannel().flush();
// Make sure to reset the timer
keepAlive.reset();
}
handlePacketDuringCaptcha(packet);
return;
}
@@ -300,11 +317,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// Check if the time limit has exceeded
if (login.elapsed(timeout)) {
user.getChannel().close();
return;
}
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
checkFrame(!login.elapsed(timeout), "time limit exceeded");
if (packet instanceof LoginAcknowledged) {
// Check if we are currently expecting a LoginAcknowledged packet
@@ -374,7 +389,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
// Validate the client branding using a regex to filter unwanted characters.
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
}
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
if (state == State.PLUGIN_MESSAGE) {
@@ -454,7 +471,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
// Then, check if the position is equal to the spawn position
&& x == SPAWN_X_POSITION && y == dynamicSpawnYPosition && z == SPAWN_Z_POSITION) {
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
// Reset all values to ensure safety on teleport
tick = 1;
posY = -1;
@@ -478,7 +495,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
listenForMovements = true;
}
lastY = dynamicSpawnYPosition;
lastY = spawnYPosition;
return;
}
@@ -493,7 +510,10 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double collisionOffsetY = DEFAULT_Y_COLLIDE_POSITION - y;
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
}
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
@@ -502,15 +522,27 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
if (tick > maxMovementTick) {
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
username, x, y, z, deltaY, ground, collisionOffsetY);
}
// The player is colliding to blocks, finish verification
if (ground) {
// If the collision check is disabled, finish verification
if (!Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions()) {
captchaOrFinish();
return;
}
// Perform the collision check
if (ground) {
// Make sure the player is actually colliding with the blocks and not only spoofing ground
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
// The player is colliding to blocks, finish verification
captchaOrFinish();
return;
} else {
// Make sure the player is colliding with blocks but is sending an invalid ground state
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
}
} else {
// Check if the player is spoofing the ground state
checkFrame(!ground, "spoofed ground state");
@@ -525,7 +557,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
username, x, y, z, deltaY, predictedY, offsetY);
}
@@ -537,7 +569,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void captchaOrFinish() {
if (user.getFallback().shouldDoMapCaptcha()) {
if (FALLBACK.shouldDoMapCaptcha()) {
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
@@ -581,7 +613,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private void finish() {
state = State.SUCCESS;
user.getFallback().getConnected().remove(username);
FALLBACK.getConnected().remove(username);
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
@@ -594,7 +626,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
@@ -31,11 +31,11 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser<?> user;
private final FallbackUser user;
private final FallbackPacketListener listener;
private FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketDecoder(final @NotNull FallbackUser<?> user,
public FallbackPacketDecoder(final @NotNull FallbackUser user,
final @NotNull FallbackPacketListener listener) {
this.user = user;
this.listener = listener;
@@ -69,6 +69,7 @@ public class FallbackPreparer {
// Blocks
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
public BlockType blockType = BlockType.BARRIER;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
@@ -109,7 +110,7 @@ public class FallbackPreparer {
DEFAULT_Y_COLLIDE_POSITION,
z + (BLOCKS_PER_ROW / 2),
0, 0);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, blockType);
}
}
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.common.fallback.protocol.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
@@ -26,7 +27,7 @@ import java.util.function.Function;
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE(protocolVersion -> 1),
STONE(protocolVersion -> 1, 1D),
BARRIER(protocolVersion -> {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
@@ -79,11 +80,14 @@ public enum BlockType {
case MINECRAFT_1_20_3:
return 10366;
}
});
}, 1D);
private final Function<ProtocolVersion, Integer> function;
private final Function<ProtocolVersion, Integer> idFunction;
// TODO: Implement per-version block heights?
@Getter
private final double blockHeight;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return function.apply(protocolVersion);
return idFunction.apply(protocolVersion);
}
}
@@ -61,7 +61,7 @@ public final class DimensionRegistry {
}
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/codecs/" + fileName)) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/assets/codecs/" + fileName)) {
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
@@ -53,7 +53,7 @@ public final class PreparedMapInfo {
this.modern = new MapData(info);
}
public void write(final @NotNull FallbackUser<?> user) {
public void write(final @NotNull FallbackUser user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
for (final MapData data : legacy) {
user.delayedWrite(data);
@@ -37,11 +37,10 @@ public final class Position implements FallbackPacket {
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.skipBytes(8);
}
y = byteBuf.readDouble();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
}
z = byteBuf.readDouble();
onGround = byteBuf.readBoolean();
}
@@ -42,11 +42,10 @@ public final class PositionLook implements FallbackPacket {
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.skipBytes(8);
}
y = byteBuf.readDouble();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
}
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
pitch = byteBuf.readFloat();
@@ -1,42 +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.common.fallback.traffic;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
@ChannelHandler.Sharable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class IncomingBandwidthDecoder extends ChannelInboundHandlerAdapter {
public static final IncomingBandwidthDecoder INSTANCE = new IncomingBandwidthDecoder();
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
TrafficCounter.INCOMING.increment(((ByteBuf) msg).readableBytes());
}
ctx.fireChannelRead(msg);
}
}
@@ -1,37 +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.common.fallback.traffic;
import io.netty.channel.ChannelPipeline;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
@UtilityClass
public class TrafficChannelHooker implements FallbackPipelines {
public void hook(final @NotNull ChannelPipeline pipeline,
final @NotNull String decoder,
final @NotNull String encoder) {
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
}
}
@@ -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;
}
@@ -23,8 +23,8 @@ 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.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
@@ -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()))
@@ -105,10 +105,10 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
case NETWORK: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
.replace("%incoming%", TrafficCounter.INCOMING.getCachedSecond())
.replace("%outgoing%", TrafficCounter.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", TrafficCounter.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", TrafficCounter.OUTGOING.getCachedTtl()));
.replace("%incoming%", Bandwidth.INCOMING.getCachedSecond())
.replace("%outgoing%", Bandwidth.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", Bandwidth.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", Bandwidth.OUTGOING.getCachedTtl()));
break;
}
}
@@ -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
}
}
@@ -0,0 +1,173 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Potřebujete pomoc nebo máte otázky? https://jonesdev.xyz/discord
# Zdrojový kód: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development a Sonar Contributors, licencováno pod GPL-3.0
#
# Obecné nastavení pro Sonar
general:
# Měl by Sonar vypisovat IP adresy hráču do konzole?
log-player-addresses: true
# Maximum hráču online se stejnou IP adresou
max-online-per-ip: 3
# Nastavení citlivosti pro sledovač útoku
# Změnte jenom pokud víte co děláte!
attack-tracker:
# Minimum nových hráču aby sledovač zaregistroval útok
min-players-for-attack: 8
# Čas, který musí uběhnout aby byl útok zaregistrovan jako ukončený
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
min-attack-duration: 30000
# Počet, kolikrát musí být incident reportován, aby byl označen jako útok
# Toto číslo figuruje jako buffer (tlumič) na filtrovaní chybných notifikací
min-attack-threshold: 2
# Čas, který musí uběhnout aby byl další útok detekován
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
attack-cooldown-delay: 3000
# Můžete se připojit Sonar k databázi aby uložil ověřené hráče ponechal i po restartu serveru
# Pozn.: IP adresy nejsou jakkoliv šifrovány, naopak jsou ponechány jako čistý text! Je vaše zodpovědnost ponechat tuto databázi bezpečnou!
# Nicméně, IP adresy nemůžou být vystopovány zpátky k hráči, protože Sonar používá UUID na ukládní místo jmeno hráčů
database:
# Typy datábázi se kterýma Sonar bude pracovat
# Typy: NONE, MYSQL (Žádný, MYSQL)
type: NONE
# Host adresa pro verifikování hráčů s SQL databází
host: localhost
# Port pro verifikaci s databází
port: 3306
# Jméno SQL databáze
name: sonar
# Jméno pro verifikaci přes SQL databázi
username: ''
# Heslo pro verifikaci přes SQL databázi
password: ''
# Kolik dní by měl Sonar ponechat hráče v databázi?
maximum-age: 5
# Každý nový login bude přidán do řady kvuli prevenci spam útoků
# Řada je aktulizována každých 500 milisekund (10 ticků)
queue:
# Maximum součastných aktualizací řady za 500 milisekund
max-polls: 30
# Každý připojený hráč bude přepojen do
# Jednoduchého limbo serveru kde proběhnou zkoušky proti botům
verification:
# Kdy by měl Sonar verifikovat nové hráče?
# TYPY: ALWAYS, DURING_ATTACK, NEVER (Vždy, Pouze při útoku, Nikdy)
# - ALWAYS: Noví hráči budou vždy verifikováni (Doporučeno)
# - DURING_ATTACK: New players will only be checked during an attack Noví hráči budou verifikováni pouze při útoku
# - NEVER: Noví hráči nebudou nikdy vyžkoušeni
timing: ALWAYS
checks:
# Kontroluje, zda pohyb hráčů je roven gravitační formuli Minecraftu
# Všechny předpokládané pohyby jsou předpočítány, kvůli ušetření výkonu
gravity:
# Měl by Sonar zkoušet pro gravitaci hráče (Doporučováno)
enabled: true
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
check-collisions: true
# Maximální počet ticků, který má hráč padat aby byl puštěn na verifikační platformu
max-movement-ticks: 8
# Maximum ignorovaných změn Y-nové souřadnice hráče, aby neprošel verifikací
max-ignored-ticks: 5
# Gamemode hráče při verifikaci
# Typy: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: všechny komponenty UI jsou zobrazeny
# - CREATIVE: životy a hunger neni zobrazen
# - ADVENTURE: všechny komponenty UI jsou zobrazeny
gamemode: ADVENTURE
# Nechá hráče verifikovat pomocí kódu na mapě ve hře
map-captcha:
# Kdy by měl Sonar nechat hráče verifikovat pomocí mapy?
# Typy: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Noví hráči budou vždy verifikováni pomocí CAPTCHY?
# - DURING_ATTACK: Noví hráči dostanou map captchu jenom při útoku
# - NEVER: Noví hráči nikdy nedostanou map CAPTCHU (Doporučeno)
timing: NEVER
# Měl by Sonar randomizovat pozici X a Y na CAPTCHE?
random-position: true
# Jak moc by měl Sonar znehodnotit charaktery (faktor pro randomizaci)
distortions-factor-x: 50
distortions-factor-y: 50
# Kolik náhodných čar by měl Sonar vykreslit za CAPTCHOU
random-lines: 4
# Kolik náhodných oválů by měl Sonar vykreslit za CAPTCHOU
random-ovals: 1
# Měl by Sonar randomizovat font pro vykreslování textu v CAPTCHE?
random-font-size: true
# Kolik odpovědí by měl Sonar předgenerovat?
# Tento úkol běží souběžně v pozadí;
# Hráči jsou schopni se připojit pokud je CAPTCHA připravena
precompute: 1000
# Jak dlouho by měl Sonar čekat než hráč selže CAPTCHU?
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
max-duration: 45000
# Kolikrát musí hráč selhat CAPTCHU před celkovém selhání verifikace?
max-tries: 3
# Znaky (čísla a písmena), které jsou povoleny k zobrazování v CAPTCHE
dictionary: '123456789'
# Jaké typy fontů by měl Sonar používat pro map CAPTCHU?
fonts:
- Dialog
- DialogInput
- Serif
- SansSerif
# Zkouší pokuď hráč posílá správný "client brand packet" serveru
client-brand:
# Měl by Sonar zkoušet pro validitu "client brandu"? (Doporučeno)
enabled: true
# Regex pro validitaci "client brand"
valid-regex: ^[!-~ ]+$
# Maximální délka "client brand" při verifikaci
max-length: 64
# Regex for validování jmen při verifikaci
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex for validaci "client locale" při verifikaci
valid-locale-regex: ^[a-zA-Z_]+$
# Maximální odezva hráče aby byl odpojen
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
max-ping: 10000
# Maximální počet login packetů, který musí hřáč poslat aby byl vyhozen
max-login-packets: 256
# Měl by Sonar posílat nové verifikace?
log-connections: true
# Měl by Sonar posílat nové verifikace při útoku?
log-during-attack: false
# Měl by Sonar posílat každý jeden pohyb při verifikaci?
# Toto nastavení není doporučeno pro servry v produkci, ale může být nápomocné při vyhledávaní chyb
debug-xyz-positions: false
# Množství času který musí uběhnout aby hráč byl odpojen
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
read-timeout: 3500
# How long should a player wait before reconnecting during verification Jak dlouho by měl hráč
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
rejoin-delay: 8000
# Jak dlouho by měla být IP adresa zakázána k připojení po selhání verifikace mockrát?
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
blacklist-time: 600000
# Kolikrát musí hráč selhat verifikaci aby byl přídán do černé listiny
# (Pokuď černou listinu nechcete dejte tuto hodnotu na 0)
blacklist-threshold: 2
# List ID protokolů (verzí), které nebudou kontrolovány Sonarem (bypass verifikace)
# Celý list ID protokolů můžete najít zde
# https://wiki.vg/Protocol_version_numbers
# Například, Minecraft 1.20 má ID 763.
whitelisted-protocols: []
# List ID protokolů které se nebudou moct připojit vůbec
blacklisted-protocols: []
@@ -0,0 +1,172 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Need help or have questions? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# General settings for Sonar
general:
# Should Sonar log players' IP addresses in console?
log-player-addresses: true
# Maximum number of players online with the same IP address
max-online-per-ip: 3
# Sensitivity settings for the attack tracker
# Only edit this if you know what you're doing
attack-tracker:
# Minimum number of new players in order for an attack to be detected
min-players-for-attack: 8
# Amount of time that has to pass in order for an attack to be over
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
min-attack-duration: 30000
# Number of times an incident has to be reported in order to be acknowledged as an attack
# This number acts as a buffer to filter out false attack notifications
min-attack-threshold: 2
# Amount of time that has to pass in order for a new attack to be detected
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
attack-cooldown-delay: 3000
# You can connect Sonar to a database to keep verified players even after restarting your server
# Note: IP addresses are saved in plain text. You are responsible for keeping your database safe!
database:
# Type of database Sonar uses to store verified players
# Possible types: NONE, MYSQL
type: NONE
# Host address for authenticating with the SQL database
host: localhost
# Port for authenticating with the SQL database
port: 3306
# Name of the SQL database
name: sonar
# Username for authenticating with the SQL database
username: ''
# Password for authenticating with the SQL database
password: ''
# How many days should Sonar keep verified players in the database?
maximum-age: 5
# Every new login request will be queued to avoid spam join attacks
# The queue is updated every 500 milliseconds (10 ticks)
queue:
# Maximum number of concurrent queue polls per 500 milliseconds
max-polls: 30
# Every new player that joins for the first time will be sent to
# a lightweight limbo server where advanced bot checks are performed
verification:
# When should Sonar verify new players?
# Possible types: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: New players will always be checked (Recommended)
# - DURING_ATTACK: New players will only be checked during an attack
# - NEVER: New players will never be checked
timing: ALWAYS
checks:
# Checks if the players' falling motion is following Minecraft's gravity formula
# All predicted motions are precalculated in order to save performance
gravity:
# Should Sonar check for valid client gravity? (Recommended)
enabled: true
# Should Sonar check if the player collides with blocks? (Recommended)
check-collisions: true
# Maximum number of ticks the player has to fall in order to be allowed to hit the platform
max-movement-ticks: 8
# Maximum number of ignored Y movement changes before a player fails verification
max-ignored-ticks: 5
# The gamemode of the player during verification
# Possible types: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: all UI components are visible
# - CREATIVE: health and hunger are hidden
# - ADVENTURE: all UI components are visible
gamemode: ADVENTURE
# Make the player type a code from a virtual map in chat
map-captcha:
# When should Sonar make the player solve a CAPTCHA?
# Possible types: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: New players will always receive a CAPTCHA
# - DURING_ATTACK: New players will only receive a CAPTCHA during an attack
# - NEVER: New players will never receive a CAPTCHA (Recommended)
timing: NEVER
# Should Sonar randomize the X and Y position of the CAPTCHA?
random-position: true
# How much should Sonar distort characters (factor for randomization)?
distortions-factor-x: 50
distortions-factor-y: 50
# How many random lines behind the CAPTCHA should Sonar draw?
random-lines: 4
# How many random ovals behind the CAPTCHA should Sonar draw?
random-ovals: 1
# Should Sonar randomize the size of the font used for rendering the CAPTCHA?
random-font-size: true
# How many answers should Sonar prepare?
# This task happens asynchronously in the background;
# Players are able to join once one CAPTCHA has been prepared
precompute: 1000
# How long should Sonar wait until the player fails the CAPTCHA?
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
max-duration: 45000
# How many times must a player fail the CAPTCHA before failing the verification?
max-tries: 3
# Characters (letters and numbers) that are allowed to appear in the answer to the CAPTCHA
dictionary: '123456789'
# Which font types should Sonar use for the map CAPTCHA codes?
fonts:
- Dialog
- DialogInput
- Serif
- SansSerif
# Checks if the players is sending a valid client brand to the server
client-brand:
# Should Sonar check for valid client brands? (Recommended)
enabled: true
# Regex for validating client brands during verification
valid-regex: ^[!-~ ]+$
# Maximum client brand length during verification
max-length: 64
# Regex for validating usernames during verification
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex for validating client locale during verification
valid-locale-regex: ^[a-zA-Z_]+$
# Ping a player has to have in order to timeout
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
max-ping: 10000
# Maximum number of login packets the player has to send in order to be kicked
max-login-packets: 256
# Should Sonar log new verification attempts?
log-connections: true
# Should Sonar log new verification attempts during attacks?
log-during-attack: false
# Should Sonar log every single movement/position change during verification?
# This is not recommended for production servers but can be helpful for spotting errors.
debug-xyz-positions: false
# Amount of time that has to pass before a player times out
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
read-timeout: 3500
# How long should a player wait before reconnecting during verification
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
rejoin-delay: 8000
# How long should an IP address be denied from logging in when failing the verification too often?
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
blacklist-time: 600000
# How often does a player have to fail the verification in order to be blacklisted?
# (Set this value to 0 to disable the blacklist entirely)
blacklist-threshold: 2
# List of protocol IDs which are not checked by Sonar (verification bypass)
# You can find the full list of all protocol IDs here:
# https://wiki.vg/Protocol_version_numbers
# For example, Minecraft 1.20 has the ID 763.
whitelisted-protocols: []
# List of protocol IDs which are unable to join the server at all
blacklisted-protocols: []
@@ -0,0 +1,172 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# დახმარება გჭირდება ან კითხვა გაქვს? https://jonesdev.xyz/discord
# საწყისი კოდი: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# ზოგადი პარამეტრები Sonar-ისთვის
general:
# უნდა ჩაწეროს თუ არა Sonar-მა მოთამაშეების IP მისამართები კონსოლში?
log-player-addresses: true
# მაქსიმალური ერთი და იგივე IP ზე ონლაინ მოთამაშეთა რაოდენობა
max-online-per-ip: 3
# მნიშვნელოვანი პარამეტრები თავდასხმის დაფიქსირებისთვის
# შეცვალეთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
attack-tracker:
# ახალი მოთამაშეების მინიმალური რაოდენობა, რათა დაფიქსირდეს შეტევა
min-players-for-attack: 8
# დრო, რის შემდეგაც შეტევა დასრულებული ჩაითვლება
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
min-attack-duration: 30000
# რამდენჯერ უნდა მოხდეს ზედა პარამეტრები, რათა ჩაითვალოს ეს თავდასხმად
# ეს რიცხვი მოქმედებს ყალბი თავდასხმის შეტყობინებების გასაფილტრად
min-attack-threshold: 2
# დრო, რომელიც უნდა გავიდეს ახალი შეტევის დასადგენად
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
attack-cooldown-delay: 3000
# თქვენ შეგიძლიათ დააკავშიროთ Sonar მონაცემთა ბაზას, რომ შეინახოთ ვერიფირებული მოთამაშეები თქვენი სერვერის გადატვირთვის შემდეგაც კი
# შენიშვნა: IP მისამართები ინახება უბრალო ტექსტში. თქვენ ხართ პასუხისმგებელი თქვენი მონაცემთა ბაზის უსაფრთხოებაზე!
database:
# მონაცემთა ბაზის ტიპები, რომლებიც Sonar-ს შეუძლია გამოიყენოს ვერიფირებული მოთამაშეების შესანახად
# შესაძლო ტიპები: NONE, MYSQL
type: NONE
# ჰოსტის მისამართი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
host: localhost
# პორტი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
port: 3306
# SQL მონაცემთა ბაზის სახელი
name: sonar
# მომხმარებლის სახელი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
username: ''
# პაროლი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
password: ''
# რამდენი დღე უნდა შეინახოს Sonar-მა დამოწმებული მოთამაშეები მონაცემთა ბაზაში?
maximum-age: 5
# ყოველი ახალი შესვლის მოთხოვნა იქნება რიგში, რათა თავიდან აიცილოთ სპამის მიერთების შეტევები
# რიგი განახლდება ყოველ 500 მილიწამში (10 ტიკი)
queue:
# ახალი მოთამაშეების მაქსიმალური რაოდენობა, რომლებსაც შეუძლიათ შესვლა სერვერზე ყოველ 500 მილიწამში
max-polls: 30
# ყოველი ახალი მოთამაშე, რომელიც პირველად შეუერთდება, გაიგზავნება
# ლიმბო სერვერში, სადაც მოთამაშეების ვერიფიკაცია ხდება
verification:
# როდის უნდა გადაამოწმოს Sonar-მა ახალი მოთამაშეები?
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: ახალი მოთამაშეები ყოველთვის მოწმდება (რეკომენდებულია)
# - DURING_ATTACK: ახალი მოთამაშეები შემოწმდებიან მხოლოდ შეტევის დროს
# - NEVER: ახალი მოთამაშეები არასოდეს შემოწმდებიან
timing: ALWAYS
checks:
# ამოწმებს, მიჰყვება თუ არა მოთამაშეების დაცემის მოძრაობა Minecraft-ის გრავიტაციის ფორმულას
# ყველა პროგნოზირებული მოძრაობა წინასწარ არის გათვლილი პერფორმანსის შესანარჩუნებლად
gravity:
# უნდა შეამოწმოს თუ არა Sonar-მა კლიენტის გრავიტაცია? (რეკომენდირებულია)
enabled: true
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს?
check-collisions: true
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს
max-movement-ticks: 8
# იგნორირებული Y მოძრაობის მაქსიმალური რაოდენობა სანამ, მოთამაშე ვერიფიკაციას ვერ გაივლის
max-ignored-ticks: 5
# მოთამაშის თამაშის რეჟიმი გადამოწმების დროს
# შესაძლო ტიპები: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: ყველა UI კომპონენტი ჩანს
# - CREATIVE: სიცოცხლის და საჭმლის მაჩვენებლები იშლება
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
gamemode: ADVENTURE
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებად
map-captcha:
# როდის უნდა გააკეთებინოს Sonar-მა მოთამაშეს CAPTCHA?
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: ახალი მოთამაშეები ყოველთვის მოუწევთ CAPTCHA-ს გაკეთება
# - DURING_ATTACK: ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს
# - NEVER: ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended)
timing: NEVER
# მოახდინოს თუ არა Sonar-მა CAPTCHA-ს X და Y პოზიციების რანდომიზაცია?
random-position: true
# რამდენად უნდა დაამახინჯოს Sonar-მა სიმბოლოები (რანდომიზაციის ფაქტორი)?
distortions-factor-x: 50
distortions-factor-y: 50
# რამდენი რანდომული ხაზი უნდა დახატოს Sonar-მა CAPTCHA-ს უკან?
random-lines: 4
# რამდენი რანდომული ოვალი უნდა დახატოს სონარმა CAPTCHA-ს უკან?
random-ovals: 1
# დაარანდომაიზოს Sonar-მა თუ არა შერჩევისას შრიფტის ზომა, რომელიც გამოიყენება CAPTCHA-ზე?
random-font-size: true
# რამდენი პასუხი უნდა მოამზადოს Sonar-მა?
# ეს ხდება ასინქრონულად უკანა ფონზე;
# მოთამაშეებს შეუძლიათ შესვლა, როდესაც ერთი CAPTCHA მზად არის
precompute: 1000
# რამდენ ხანს უნდა დაელოდოს Sonar-ი, სანამ მოთამაშე ვერ გააკეთებს წარმატებულად CAPTCHA-ს?
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
max-duration: 45000
# რამდენჯერ ვერ უდნა გაიაროს CAPTCHA მოთამაშემ სანამ ვერიფიკაციას ვერ გაივლის?
max-tries: 3
# სიმბოლოები (ასოები და რიცხვები), რომლებიც დაშვებულია გამოჩნდეს CAPTCHA-ს პასუხში
dictionary: '123456789'
# რომელი შრიფტის ტიპები უნდა გამოიყენოს Sonar-მა რუკის CAPTCHA კოდებისთვის?
fonts:
- Dialog
- DialogInput
- Serif
- SansSerif
#ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ კლიენტის სახელს
client-brand:
# უნდა შეამოწმოს თუ არა Sonar-მა ნამდვილია თუ არა მოთამაშის კლიენტის სახელი? (რეკომენდებულია)
enabled: true
# Regex-ი კლიენტის ბრენდების დასადასტურებლად ვერიფიკაციის დროს
valid-regex: ^[!-~ ]+$
# მოთამაშის კლიენტის სახელის მაქსიმალური სიგრძე ვერიფიკაციის დროს
max-length: 64
# Regex-ი მომხმარებლის სახელების ვერიფიკაციისთვის
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex-ი კლიენტის ლოკალის დასადასტურებლად ვერიფიკაციის დროს
valid-locale-regex: ^[a-zA-Z_]+$
# პინგი რამდენიც უნდა ქონდეს მოთამაშეს იმისთვის, რომ ტაიმაუტდეს
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
max-ping: 10000
# მაქსიმალური რაოდენობა ლოგინ პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გაიკიკება სერვერიდან
max-login-packets: 256
# ჩაიწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები?
log-connections: true
# ჩაწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები შეტევის დროს?
log-during-attack: false
# ჩაიწეროს თუ არა Sonar-მა ყოველი მოძრაობა/პოზიცია ვერიფიკაციის დროს?
# ეს არ არის რეკომენდირებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
debug-xyz-positions: false
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
read-timeout: 3500
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
rejoin-delay: 8000
# რამდენ ხნით უნდა აეკრძალოს IP მისამართს შესვლა, როდესაც ვერიფიკაციას ძალიან ხშირად ვერ აკეთებს?
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
blacklist-time: 600000
# რამდენად ხშირად უნდა გაიაროს მოთამაშემ ვერიფიკაცია წარუმატებლად, რომ ის მოხვდეს შავ სიაში?
# (დააყენეთ ეს 0 ზე, რომ მთლიანად გათიშოთ შავი სია)
blacklist-threshold: 2
# პროტოკოლის ID-ების სია, რომლებიც არ არის შემოწმებული Sonar-ის მიერ (დამოწმების შემოვლითი გზა)
# თქვენ შეგიძლიათ იპოვოთ ყველა პროტოკოლის ID-ების სრული სია აქ:
# https://wiki.vg/Protocol_version_numbers
# მაგალითად, Minecraft 1.20-ს აქვს ID 763.
whitelisted-protocols: []
# პროტოკოლის ID-ების სია, რომლებსაც საერთოდ არ შეუძლიათ სერვერზე შემოსვლა
blacklisted-protocols: []
@@ -0,0 +1,172 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
# Исходный код: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Основные настройки
general:
# Должен ли Sonar логировать IP-адреса игроков в консоли?
log-player-addresses: true
# Максимальное количество игроков онлайн с одного IP-адреса
max-online-per-ip: 3
# Настройки чувствительности для трекера атак
# Редактируйте это только в том случае, если вы знаете, что делаете.
attack-tracker:
# Минимальное количество новых игроков, чтобы атака была обнаружена
min-players-for-attack: 8
# Количество времени, которое должно пройти, чтобы атака закончилась
# (Это значение представляет собой время в миллисекундах: 1 секунда = 1000 миллисекунд)
min-attack-duration: 30000
# Количество раз, которое необходимо сообщить об инциденте, чтобы он был признан атакой.
# Это число служит буфером для отсеивания ложных уведомлений об атаках
min-attack-threshold: 2
# Количество времени, которое должно пройти, чтобы новая атака была обнаружена.
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
attack-cooldown-delay: 3000
# Вы можете подключить Sonar к базе данных, чтобы сохранять проверенных игроков даже после перезапуска сервера.
# Примечание: IP-адреса сохраняются в виде обычного текста. Вы сами отвечаете за сохранность своей базы данных!
database:
# Тип базы данных, которую Sonar использует для хранения проверенных игроков.
# Возможные типы: NONE, MYSQL
type: NONE
# Адрес хоста для аутентификации в базе данных SQL
host: localhost
# Порт для аутентификации с базой данных SQL
port: 3306
# Имя базы данных SQL
name: sonar
# Имя пользователя для аутентификации в базе данных SQL
username: ''
# Пароль для аутентификации в базе данных SQL
password: ''
# Сколько дней Sonar должен хранить проверенных игроков в базе данных?
maximum-age: 5
# Каждый новый запрос на вход на сервер будет ставиться в очередь, чтобы избежать спам-атак.
# Очередь обновляется каждые 500 миллисекунд (10 тиков)
queue:
# Максимальное количество одновременных опросов очереди за 500 миллисекунд
max-polls: 30
# Каждый новый игрок, впервые присоединившийся на сервер, будет отправлен на
# лимбо-сервер, на котором проводятся расширенные проверки ботов
verification:
# Когда Sonar должен проверять новых игроков?
# Возможные типы: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Новые игроки будут проверяться всегда (Рекомендуется)
# - DURING_ATTACK: Новые игроки будут проверяться только во время атаки
# - NEVER: Новые игроки никогда не будут проверены
timing: ALWAYS
checks:
# Проверяет, следует ли движение игрока формуле гравитации Minecraft.
# Все предсказанные движения предварительно просчитываются для экономии производительности
gravity:
# Должен ли Sonar проверять наличие действительной гравитации клиента? (Рекомендуется)
enabled: true
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
check-collisions: true
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
max-movement-ticks: 8
# Максимальное количество проигнорированных изменений движения Y, прежде чем игрок не пройдет проверку
max-ignored-ticks: 5
# Режим игрока во время проверки
# Возможные типы: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: все компоненты пользовательского интерфейса видны
# - CREATIVE: здоровье и голод скрыты
# - ADVENTURE: все компоненты пользовательского интерфейса видны
gamemode: ADVENTURE
# Заставить игрока ввести код с картинки в чат
map-captcha:
# Когда Sonar должен заставить игрока решить CAPTCHA?
# Возможные типы: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Новые игроки всегда получают капчу
# - DURING_ATTACK: Новые игроки будут получать капчу только во время атаки
# - NEVER: Новые игроки никогда не получат капчу (Рекомендуется)
timing: NEVER
# Должен ли Sonar рандомизировать положение X и Y капчи?
random-position: true
# Насколько сильно Sonar должен искажать символы (фактор для рандомизации)?
distortions-factor-x: 50
distortions-factor-y: 50
# Сколько случайных линий за капчей должен нарисовать Sonar?
random-lines: 4
# Сколько случайных овалов за капчей должен нарисовать Sonar?
random-ovals: 1
# Должен ли Sonar рандомизировать размер шрифта, используемого для отображения капчи?
random-font-size: true
# Сколько капч должен подготовить Sonar?
# Эта задача выполняется асинхронно в фоновом режиме
# Игроки смогут присоединиться, как только будет подготовлена одна капча
precompute: 1000
# Как долго Sonar должен ждать, пока игрок не пройдет капча?
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
max-duration: 45000
# Сколько раз игрок должен завалить капчу, чтобы не пройти проверку?
max-tries: 3
# Символы (буквы и цифры), которые разрешено использовать в ответе на капчу
dictionary: '123456789'
# Какие типы шрифтов следует использовать в Sonar для текста в капче?
fonts:
- Dialog
- DialogInput
- Serif
- SansSerif
# Проверяет, отправляет ли игрок на сервер действительный клиентский бренд.
client-brand:
# Должен ли Sonar проверять наличие действительных клиентских брендов? (Рекомендуется)
enabled: true
# Regex для проверки брендов клиентов во время верификации
valid-regex: ^[!-~ ]+$
# Максимальная длина бренда клиента при проверке
max-length: 64
# Regex для проверки ников при верификации
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex для проверки локализации клиента при верификации
valid-locale-regex: ^[a-zA-Z_]+$
# Пинг который должен быть у игрока чтобы не пройти проверку
# (Это значение представляет собой время в миллисекундах: 1 секунда = 1000 миллисекунд)
max-ping: 10000
# Максимальное количество пакетов для входа на сервер, которое игрок должен отправить, чтобы быть выгнанным
max-login-packets: 256
# Должен ли Sonar регистрировать новые попытки проверки?
log-connections: true
# Должен ли Sonar регистрировать новые попытки проверки во время атак?
log-during-attack: false
# Должен ли Sonar регистрировать каждое движение/изменение положения во время проверки?
# Это не рекомендуется для рабочих серверов, но может быть полезно для выявления ошибок.
debug-xyz-positions: false
# Количество времени, которое должно пройти, прежде чем игрока кикнет.
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
read-timeout: 3500
# Сколько времени игрок должен ждать перед повторным подключением во время проверки
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
rejoin-delay: 8000
# Как долго IP-адресу должно быть запрещено входить на сервер, если он слишком часто не проходит проверку?
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
blacklist-time: 600000
# Как часто игрок должен проваливать проверку, чтобы попасть в черный список?
# (Установите это значение на 0, чтобы полностью отключить черный список)
blacklist-threshold: 2
# Список протоколов, которые не проверяются Sonar (обход проверки)
# Полный список всех идентификаторов протоколов можно найти здесь:
# https://wiki.vg/Protocol_version_numbers
# Например, Minecraft 1.20 имеет ID 763.
whitelisted-protocols: []
# Список протоколов, которые вообще не могут присоединиться к серверу
blacklisted-protocols: []
@@ -0,0 +1,30 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Need help or have questions? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Which language should Sonar use for all configurations?
# For this to take effect, restart your server or reload Sonar.
#
# Values will not be translated if you change your language
# when all configuration files already exist.
# If you still want all configuration files to be translated,
# you have to reset the configuration, messages, and webhook settings.
#
# You can view a full list of valid language codes here:
# https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
# If a translation does not exist yet, Sonar will use English (en)
#
# If you set this option to "system", Sonar will use the system language.
# Please keep this file unchanged if you don't know what you're doing.
language=system
@@ -0,0 +1,330 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Need help or have questions? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Placeholder for every '%prefix%' in this configuration file
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Placeholder for every '%support-url%' in this configuration file
support-url: https://jonesdev.xyz/discord/
# Placeholder for every '%header%' in this configuration file
header:
- <b><yellow>Sonar</b><reset>
- ''
# Placeholder for every '%footer%' in this configuration file
footer:
- <gray>If you believe that this is an error, contact an administrator.
# Disconnect message that is shown when someone joins but there are too many online players with their IP address
too-many-online-per-ip:
- '%header%'
- <red>There are too many players online with your IP address.
- '%footer%'
# Translations for commands
commands:
# Message that is shown when a player tries running /sonar without permission
no-permission: '%prefix%<red>You do not have permission to execute this command.'
# Message that is shown when someone uses a command incorrectly
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
console-only: '%prefix%<red>For security reasons, you can only execute this command
through console.'
# Message that is shown when a player executes Sonar commands too quickly
cool-down: '%prefix%<red>You can only execute this command every 0.5 seconds.'
cool-down-left: '%prefix%<red>Please wait another <bold>%time-left%s<reset><red>.'
# Message that is shown when a player does not have permission to execute a certain subcommand
subcommand-no-permission: '%prefix%<red>You do not have permission to execute this
subcommand. <gray>(%permission%)'
# Translations for '/sonar'
main:
# Informational message that is shown above everything when running the main command
header:
- <yellow>Running <b>Sonar</b> %version% on %platform%.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
Jones Development and Sonar Contributors</click>
- ''
- <gray>Need help or have any questions?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click
to open Discord)'>Open a ticket on the Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click
to open GitHub)'>or open a new issue on GitHub.
- ''
# Formatting of the list of subcommands shown when running the main command
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Only
players: </gray>%only_players%<br><gray>Require console: </gray>%require_console%<br><gray>Permission:
</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%''><dark_aqua>
▪ <gray>/sonar %subcommand% <white>%description%'
# Translations for '/sonar reload'
reload:
# Message that is shown when someone starts reloading Sonar
start: '%prefix%Reloading Sonar...'
# Message that is shown when Sonar has finished reloading
finish: '%prefix%<green>Successfully reloaded <gray>(%taken%ms)'
# Translations for '/sonar verbose'
verbose:
# Message that is shown when a player subscribes to Sonar verbose
subscribed: '%prefix%You are now viewing Sonar verbose.'
# Message that is shown when a player unsubscribes from Sonar verbose
unsubscribed: '%prefix%You are no longer viewing Sonar verbose.'
# Translations for '/sonar notify'
notify:
# Message that is shown when a player subscribes to Sonar attack notifications
subscribed: '%prefix%You are now viewing Sonar attack notifications.'
# Message that is shown when a player unsubscribes from Sonar attack notifications
unsubscribed: '%prefix%You are no longer viewing Sonar attack notifications.'
# Translations for '/sonar blacklist'
blacklist:
# Message that is shown when someone tries clearing the blacklist but is is empty
empty: '%prefix%The blacklist is currently empty. Therefore, no IP addresses were
removed from the blacklist.'
# Message that is shown when someone clears the blacklist
cleared: '%prefix%You successfully removed a total of %removed% IP address(es)
from the blacklist.'
# Message that is shown when someone checks the size of the blacklist
size: '%prefix%The blacklist currently contains %amount% IP address(es).'
# Message that is shown when someone adds an IP address to the blacklist
added: '%prefix%Successfully added %ip% to the blacklist.'
# Message that is shown when someone adds an IP address to the blacklist that is verified
added-warning: '%prefix%<red>Warning: <white>%ip% is currently whitelisted. Consider
removing the IP address from the list of verified players to avoid potential
issues.'
# Message that is shown when someone removes an IP address from the blacklist
removed: '%prefix%Successfully removed %ip% from the blacklist.'
# Message that is shown when someone adds an IP address to the blacklist, but it is already blacklisted
duplicate-ip: '%prefix%The IP address you provided is already blacklisted.'
# Message that is shown when someone removes an IP address from the blacklist, but it is not blacklisted
ip-not-found: '%prefix%The IP address you provided is not blacklisted.'
# Translations for '/sonar verified'
verified:
# Message that is shown when someone tries clearing the list of verified players but is empty
empty: '%prefix%The list of verified players is currently empty. Therefore, no
players were unverified.'
# Message that is shown when someone clears the list of verified players
cleared: '%prefix%You successfully unverified a total of %removed% unique player(s).'
# Message that is shown when someone checks the size of the list of verified players
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 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)
blocked: '%prefix%Please wait for the current operation to finish.'
# Translations for '/sonar statistics'
statistics:
# Informational message that is shown above everything when viewing the statistics
header: '%prefix%Showing %type% statistics for this session:'
# Message that is shown when a player tries viewing an unknown statistic
unknown-type: '%prefix%<red>Unknown statistics type! Available statistics: <gray>%statistics%'
# Format of the general statistics message
general:
- ' <dark_aqua>▪ <gray>Verified IP addresses: <white>%verified%'
- ' <dark_aqua>▪ <gray>Verifying IP addresses: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Blacklisted IP addresses: <white>%blacklisted%'
- ' <dark_aqua>▪ <gray>Currently queued logins: <white>%queued%'
- ' <dark_aqua>▪ <gray>Total non-unique joins: <white>%total_joins%'
- ' <dark_aqua>▪ <gray>Total verification attempts: <white>%total_attempts%'
- ' <dark_aqua>▪ <gray>Total failed verifications: <white>%total_failed%'
- ' <dark_aqua>▪ <gray>Server uptime: <white>%uptime%'
# Format of the CPU statistics message
cpu:
- ' <dark_aqua>▪ <gray>Process CPU usage right now: <white>%process_cpu%%'
- ' <dark_aqua>▪ <gray>System CPU usage right now: <white>%system_cpu%%'
- ' <dark_aqua>▪ <gray>Per-core process CPU usage: <white>%average_process_cpu%%'
- ' <dark_aqua>▪ <gray>Per-core system CPU usage: <white>%average_system_cpu%%'
- ' <dark_aqua>▪ <gray>General system load average: <white>%load_average%%'
- ' <dark_aqua>▪ <gray>Total amount of virtual cpus: <white>%virtual_cores%'
# Format of the memory statistics message
memory:
- ' <dark_aqua>▪ <gray>Total free memory: <white>%free_memory%'
- ' <dark_aqua>▪ <gray>Total used memory: <white>%used_memory%'
- ' <dark_aqua>▪ <gray>Total maximum memory: <white>%max_memory%'
- ' <dark_aqua>▪ <gray>Total allocated memory: <white>%total_memory%'
# Format of the network statistics message
network:
- ' <dark_aqua>▪ <gray>Current incoming used bandwidth: <white>%incoming%'
- ' <dark_aqua>▪ <gray>Current outgoing used bandwidth: <white>%outgoing%'
- ' <dark_aqua>▪ <gray>Total incoming used bandwidth: <white>%ttl_incoming%'
- ' <dark_aqua>▪ <gray>Total outgoing used bandwidth: <white>%ttl_outgoing%'
# Translations for all messages during the verification process
verification:
# Translations for all debug messages during the verification
logs:
# Message that is logged to console whenever a new player joins the server
connection: '%name%%ip% (%protocol%) has connected.'
# Message that is logged to console whenever a player fails verification
failed: '%ip% (%protocol%) has failed the bot check for: %reason%'
# Message that is logged to console whenever a player is blacklisted
blacklisted: '%ip% (%protocol%) was blacklisted for too many failed attempts'
# Message that is logged to console whenever a player is verified
successful: '%name% has been verified successfully (%time%s!).'
# Message that is shown to the player when they are being checked for valid gravity
welcome: '%prefix%<gray>Please wait a moment for the verification to finish...'
# Messages that are shown to the player when they are supposed to enter the CAPTCHA
captcha:
# Message that is shown to the player when they have to enter the answer to the captcha
enter-code: '%prefix%<green>Please enter the code in chat that is displayed on
the map.'
# Timer that is shown to the player when they have to enter the answer to the captcha
# (Set this to '' to disable the action bar message)
action-bar: '%prefix%<green>You have %time-left% seconds left to enter the code
in chat'
# Message that is shown to the player when they enter the wrong answer in chat
incorrect: '%prefix%<red>You have entered the wrong code. Please try again.'
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
currently-preparing:
- '%header%'
- <yellow>Your anti-bot data has not been prepared yet.
- <gray>Please wait a few seconds before trying to verify again.
- '%footer%'
# Disconnect message that is shown when too many players are verifying at the same time
too-many-players:
- '%header%'
- <gold>Too many players are currently trying to log in, try again later.
- <gray>Please wait a few seconds before trying to join again.
- '%footer%'
# Disconnect message that is shown when someone rejoins too fast during verification
too-fast-reconnect:
- '%header%'
- <gold>You reconnected too fast, try again later.
- <gray>Please wait a few seconds before trying to verify again.
- '%footer%'
# Disconnect message that is shown when someone joins but is already verifying
already-verifying:
- '%header%'
- <red>Your IP address is currently being verified.
- <red>Please wait a few seconds before trying to verify again.
- '%footer%'
# Disconnect message that is shown when someone joins but is already queued for verification
already-queued:
- '%header%'
- <red>Your IP address is currently queued for verification.
- <red>Please wait a few minutes before trying to verify again.
- '%footer%'
# Disconnect message that is shown when someone joins but is temporarily blacklisted
blacklisted:
- '%header%'
- <red>You are currently denied from entering the server.
- <red>Please wait a few minutes to be able to join the server again.
- <gold>False positive? <gray>%support-url%
- '%footer%'
# Disconnect message that is shown when someone joins with an invalid username
invalid-username:
- '%header%'
- <red>Your username contains invalid characters.
- '%footer%'
# Disconnect message that is shown when someone joins with a blacklisted version
blacklisted-protocol:
- '%header%'
- <red>You are using a version that is not allowed on our server.
- <gold>Need help logging in? <gray>%support-url%
- '%footer%'
# Disconnect message that is shown when someone tries verifying with an account that is online
already-online:
- '%header%'
- <red>There is someone already online with your account.
- <gray>Please wait a few seconds before trying to verify again.
- <gray>If this keeps occurring, try restarting your game or contact support.
- '%footer%'
# Disconnect message that is shown when someone verifies successfully
success:
- '%header%'
- <green>You have successfully passed the verification.
- <white>You are now able to play on the server when you reconnect.
# Disconnect message that is shown when someone fails verification
failed:
- '%header%'
- <red>You have failed the verification.
- <gray>Please wait a few seconds before trying to verify again.
- <gold>Need help? <gray>%support-url%
- '%footer%'
# Translations for Sonar's actionbar verbose messages
verbose:
layout:
# Layout for Sonar's actionbar verbose
# Placeholders:
# - '%queued%' Number of queued connections
# - '%verifying%' Number of verifying connections
# - '%blacklisted%' Number of blacklisted IP addresses
# - '%total-joins%' Number of total attempted joins
# - '%verify-total%' Number of total verification attempts
# - '%verify-success%' Number of verified IP addresses
# - '%verify-failed%' Number of failed verifications
# - '%logins-per-second%' Number of logins per second
# - '%connections-per-second%' Number of connections per second
# - '%attack-duration%' Duration of the current attack
# - '%incoming-traffic%' Incoming bandwidth usage per second
# - '%outgoing-traffic%' Outgoing bandwidth usage per second
# - '%incoming-traffic-ttl%' Total incoming bandwidth usage
# - '%outgoing-traffic-ttl%' Total outgoing bandwidth usage
# - '%used-memory%' Amount of used memory of the process
# - '%total-memory%' Amount of total memory of the process
# - '%max-memory%' Amount of max memory of the process
# - '%free-memory%' Amount of free memory of the process
# - '%animation%' Customizable animated symbol
# Translations for Sonar's normal verbose output
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Verifying <white>%verifying% <dark_aqua>|
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Layout for Sonar's actionbar verbose during an attack
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Queue <white>%queued% <dark_aqua>╺
<gray>Verifying <white>%verifying% <dark_aqua>╺ <gray>Duration <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Animation for the action bar
# Alternatives:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# Translations for Sonar's attack notification messages
notifications:
# Layout for Sonar's attack notification title
title: <yellow><b>Sonar<reset>
# Layout for Sonar's attack notification subtitle
subtitle: <gray>A bot attack has been detected
# Layout for Sonar's attack notification in chat
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>A bot attack has been detected.
- ' <dark_aqua>▪ <gray>Logins per second: <white>%logins-per-second%'
- ' <dark_aqua>▪ <gray>Verifying players: <white>%verifying%'
- ' <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><gray> or <white><click:run_command:'/sonar
stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- ''
@@ -0,0 +1,333 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
# Исходный код: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Плейсхолдер для каждого '%prefix%' в файле конфигурации
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Плейсхолдер для каждого '%support-url%' в файле конфигурации
support-url: https://jonesdev.xyz/discord/
# Плейсхолдер для каждого '%header%' в файле конфигурации
header:
- <b><yellow>Sonar</b><reset>
- ''
# Плейсхолдер для каждого '%footer%' в файле конфигурации
footer:
- <gray>Если вы считаете что это ошибка, обратитесь к администратору.
# Сообщение отключения, которое показывается, когда кто-то присоединяется, но в сети слишком много игроков с его IP-адресом
too-many-online-per-ip:
- '%header%'
- <red>С вашего IP-адреса в сети слишком много игроков.
- '%footer%'
# Перевод для команды
commands:
# Сообщение, которое будет показано, когда игрок попытается выполнить команду /sonar без разрешения
no-permission: '%prefix%<red>У вас нет прав на использование этой команды.'
# Сообщение, которое отображается при неправильном использовании команды
incorrect-usage: '%prefix%<red>Применение: /sonar %usage%'
# Сообщение, которое отображается, когда кто-то предоставляет неверный формат IP-адреса
invalid-ip-address: '%prefix%Указанный вами IP-адрес недействителен'
# Сообщение, которое отображается, когда кто-то предоставляет локальный IP-адрес
illegal-ip-address: '%prefix%IP-адрес который вы указали является локальным.'
# Сообщение, которое отображается, когда консоль выполняет команду, предназначенную только для игроков
player-only: '%prefix%<red>Эта команда может быть выполнена только игроком.'
# Сообщение, которое отображается, когда игрок выполняет команду, доступную только для консоли
console-only: '%prefix%<red>Эту команду можно выполнить только только через консоль.'
# Сообщение, которое появляется, когда игрок выполняет команды плагина слишком быстро
cool-down: '%prefix%<red>Эту команду можно использовать только каждые 0,5 секунды.'
cool-down-left: '%prefix%<red>Подождите еще <bold>%time-left%s<reset><red>.'
# Сообщение, которое отображается, когда у игрока нет разрешения на выполнение подкоманды
subcommand-no-permission: '%prefix%<red>У вас нет разрешения на выполнение этой
подкоманды. <gray>(%permission%)'
# Перевод для '/sonar'
main:
# Информационное сообщение, которое показывается поверх всего при выполнении главной команды
header:
- <yellow>Запущен <b>Sonar</b> %version% на %platform%.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
Jones Development and Sonar Contributors</click>
- ''
- <gray>Нужна помощь или есть вопросы?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Нажмите
чтобы открыть Discord)'>Откройте тикет в Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Нажмите
чтобы открыть GitHub)'>или откройте новый вопрос на GitHub.
- ''
# Форматирование списка подкоманд
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Только
игроки: </gray>%only_players%<br><gray>Требуется консоль: </gray>%require_console%<br><gray>Permission:
</gray><white>%permission%<br><gray>Псевдонимы: </gray>%aliases%''><dark_aqua>
▪ <gray>/sonar %subcommand% <white>%description%'
# Перевод для '/sonar reload'
reload:
# Сообщение, которое отображается, когда кто-то начинает перезагружать Sonar
start: '%prefix%Перезагрузка...'
# Сообщение, которое отображается, когда Sonar завершает перезагрузку
finish: '%prefix%<green>Успешная перезагрузка <gray>(%taken%ms)'
# Перевод для '/sonar verbose'
verbose:
# Сообщение, которое показывается, когда игрок подписывается на Sonar verbose
subscribed: '%prefix%Вы просматриваете подробную информацию о Sonar.'
# Сообщение, которое показывается, когда игрок отписывается от Sonar verbose
unsubscribed: '%prefix%Вы больше не просматриваете подробную информацию о Sonar.'
# Перевод для '/sonar notify'
notify:
# Сообщение, которое отображается, когда игрок подписывается на уведомления об атаках
subscribed: '%prefix%Вы получаете уведомления об атаках.'
# Сообщение, которое отображается, когда игрок отписывается на уведомления об атаках
unsubscribed: '%prefix%Вы больше не получаете уведомления об атаках.'
# Перевод для '/sonar blacklist'
blacklist:
# Сообщение, которое отображается, когда кто-то пытается очистить черный список, но он пустой
empty: '%prefix%В настоящее время черный список пуст. Поэтому ни один IP-адрес
не был удален из черного списка.'
# Сообщение, которое показывается, когда кто-то очищает черный список
cleared: '%prefix%Вы успешно удалили %удалено% IP-адресов из черного списка.'
# Сообщение, которое показывается, когда кто-то проверяет размер черного списка
size: '%prefix%В настоящее время черный список содержит %amount% IP-адрес(ов).'
# Message that is shown when someone adds an IP address to the blacklist
added: '%prefix%Успешно добавлен %ip% в черный список.'
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, который находится в белом списке
added-warning: '%prefix%<red>Предупреждение: <white>%ip% находится в белом списке.
Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы
избежать потенциальных ошибок.'
# Сообщение, которое показывается, когда кто-то удаляет IP-адрес из черного списка
removed: '%prefix%Успешно удален %ip% из черного списка.'
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, но он уже находится в черном списке
duplicate-ip: '%prefix%Указанный вами IP-адрес уже внесен в черный список.'
# Сообщение, которое отображается, когда кто-то удаляет IP-адрес из черного списка, но он не занесен в черный список
ip-not-found: '%prefix%Указанный вами IP-адрес не внесен в черный список.'
# Перевод для '/sonar verified'
verified:
# Сообщение, которое отображается, когда кто-то пытается очистить список проверенных игроков, но он пустой
empty: '%prefix%Список проверенных игроков в настоящее время пуст. Поэтому, ни один игрок не был удален.'
# Сообщение, которое отображается, когда кто-то очищает список проверенных игроков
cleared: '%prefix%Вы успешно очистили %removed% уникального игрока(ов).'
# Сообщение, которое показывается, когда кто-то проверяет размер списка проверенных игроков
size: '%prefix%В настоящее время проверено %amount% уникальных игроков.'
# Сообщение, которое отображается при удалении IP-адреса из списка проверенных игроков
removed: '%prefix%Успешно удален %ip%.'
# Сообщение, которое отображается, когда кто-то удаляет IP-адрес из списка, но его нет в списке
ip-not-found: '%prefix%Указанный вами IP-адрес не в списке.'
# Сообщение, которое отображается, когда кто-то пытается дважды удалить один и тот же IP-адрес (двойная операция)
blocked: '%prefix%Пожалуйста, дождитесь завершения текущей операции.'
# Перевод для '/sonar statistics'
statistics:
# Информационное сообщение, которое отображается поверх всего при просмотре статистики
header: '%prefix%Показываем статистику %type% для этой сессии:'
# Сообщение, которое отображается, когда игрок пытается просмотреть неизвестную статистику
unknown-type: '%prefix%<redНеизвестный тип статистики! Доступные статистики: <gray>%statistics%'
# Формат сообщения общей статистики
general:
- ' <dark_aqua>▪ <gray>Проверенные IP-адреса: <white>%verified%'
- ' <dark_aqua>▪ <gray>Проверяющиеся IP-адреса: <white>%verifying%'
- ' <dark_aqua>▪ <gray>IP-адреса в черном списке: <white>%blacklisted%'
- ' <dark_aqua>▪ <gray>Текущая очередь на проверку: <white>%queued%'
- ' <dark_aqua>▪ <gray>Всего неуникальных соединений: <white>%total_joins%'
- ' <dark_aqua>▪ <gray>Общее количество попыток проверки: <white>%total_attempts%'
- ' <dark_aqua>▪ <gray>Всего неудачных попыток провери: <white>%total_failed%'
- ' <dark_aqua>▪ <gray>Время работы сервера: <white>%uptime%'
# Формат сообщения статистики процессора
cpu:
- ' <dark_aqua>▪ <gray>Использование процессора сервером в данный момент: <white>%process_cpu%%'
- ' <dark_aqua>▪ <gray>Использование процессора системой прямо сейчас: <white>%system_cpu%%'
- ' <dark_aqua>▪ <gray>Использование процессора каждым ядром (сервером): <white>%average_process_cpu%%'
- ' <dark_aqua>▪ <gray>Использование процессора каждым ядром (системой): <white>%average_system_cpu%%'
- ' <dark_aqua>▪ <gray>Средняя нагрузка на систему: <white>%load_average%%'
- ' <dark_aqua>▪ <gray>Общее количество виртуальных ядер: <white>%virtual_cores%'
# Формат сообщения статистики памяти
memory:
- ' <dark_aqua>▪ <gray>Общее количество свободной памяти: <white>%free_memory%'
- ' <dark_aqua>▪ <gray>Общее количество используемой памяти: <white>%used_memory%'
- ' <dark_aqua>▪ <gray>Общий объем максимальной памяти: <white>%max_memory%'
- ' <dark_aqua>▪ <gray>Общее количество выделенной памяти: <white>%total_memory%'
# Формат сообщения сетевой статистики
network:
- ' <dark_aqua>▪ <gray>Текущая входящая используемая полоса пропускания: <white>%incoming%'
- ' <dark_aqua>▪ <gray>Текущая исходящая используемая полоса пропускания: <white>%outgoing%'
- ' <dark_aqua>▪ <gray>Общая входящая использованная полоса пропускания: <white>%ttl_incoming%'
- ' <dark_aqua>▪ <gray>Общая исходящая использованная полоса пропускания: <white>%ttl_outgoing%'
# Переводы всех сообщений в процессе проверки
verification:
# TПереводы всех отладочных сообщений во время проверки
logs:
# Сообщение, которое выводится в консоль каждый раз, когда к серверу присоединяется новый игрок
connection: '%name%%ip% (%protocol%) подключился.'
# Сообщение, которое выводится в консоль при неудачной проверке игрока
failed: '%ip% (%protocol%) не прошел проверку бота на: %reason%'
# Сообщение, которое выводится в консоль, когда игрок попадает в черный список
blacklisted: '%ip% (%protocol%) был занесен в черный список за слишком большое
количество неудачных попыток'
# Сообщение, которое выводится в консоль при успешной проверке игрока
successful: '%name% прошел проверку (%time%s!).'
# Сообщение, которое показывается игроку при проверке на наличие ванильной гравитации
welcome: '%prefix%<gray>Пожалуйста, подождите некоторое время для завершения проверки...'
captcha:
# Сообщение, которое показывается игроку, когда он должен ввести ответ на капчу
enter-code: '%prefix%<green>Введите код в чате, который отображается на карте.'
# Таймер, который показывается игроку, когда он должен ввести ответ на капчу
# (Установите это значение на '' что-бы отключить эту функцию)
action-bar: '%prefix%<green>У вас осталось %time-left% секунд, чтобы ввести код
в чате'
# Сообщение, которое показывается игроку, когда он вводит неправильный код капчи в чате
incorrect: '%prefix%<red>Вы ввели неправильный код. Пожалуйста, попробуйте еще
раз.'
# Сообщение об отключении, которое отображается, когда кто-то присоединяется но капча еще не подготовлена
currently-preparing:
- '%header%'
- <yellow>Капча еще не подготовлена.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- '%footer%'
# Сообщение об отключении, которое отображается при одновременной верификации слишком большого количества игроков
too-many-players:
- '%header%'
- <gold>Слишком много игроков пытаются войти на сервер.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- '%footer%'
# Сообщение об отключении, которое отображается, когда кто-то слишком быстро перезаходит во время проверки
too-fast-reconnect:
- '%header%'
- <gold>Вы перезаходите слишком быстро, повторите попытку позже.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- '%footer%'
# Сообщение об отключении, которое отображается, когда кто-то присоединяется, но его IP-адрес уже проходит верификацию
already-verifying:
- '%header%'
- <red>Ваш IP-адрес в настоящее время проверяется.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- '%footer%'
# Сообщение об отключении, которое показывается, когда кто-то присоединяется, но уже находится в очереди на проверку
already-queued:
- '%header%'
- <red>Ваш IP-адрес в настоящее время находится в очереди на проверку.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- '%footer%'
# Сообщение об отключении, которое показывается, когда кто-то присоединяется, но временно находится в черном списке
blacklisted:
- '%header%'
- <red>Вам запрещено входить на сервер.
- <red>Пожалуйста, подождите несколько минут, чтобы снова присоединиться к серверу.
- <gold>Вы считаете что это ошибка? <gray>%support-url%
- '%footer%'
# Сообщение об отключении, которое показывается, когда кто-то присоединяется с неправильным ником
invalid-username:
- '%header%'
- <red>Ваш ник содержит недопустимые символы.
- '%footer%'
# Сообщение об отключении, которое отображается, когда кто-то присоединяется с неподдерживаемой версией
invalid-protocol:
- '%header%'
- <red>Ваша версия в настоящее время не поддерживается.
- '%footer%'
# Сообщение об отключении, которое показывается, когда кто-то заходит на сервер версией, внесенной в черный список
blacklisted-protocol:
- '%header%'
- <red>Вы используете версию, которая запрещена на нашем сервере.
- <gold>Нужна помощь? <gray>%support-url%
- '%footer%'
# Сообщение об отключении, которое показывается, когда кто-то пытается пройти верификацию с помощью ника, который находится онлайн.
already-online:
- '%header%'
- <red>Пользователь с таким ником уже онлайн.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- <gray>Если это продолжается, попробуйте перезапустить игру или обратитесь к
администратору.
- '%footer%'
# Сообщение об отключении, которое показывается при успешной верификации
success:
- '%header%'
- <green>Вы успешно прошли проверку.
- <white>Пожалуйста, перезайдите.
# Сообщение об отключении, которое показывается, когда кто-то не прошел верификацию
failed:
- '%header%'
- <red>Вы не прошли проверку.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- <gold>Нужна помощь? <gray>%support-url%
- '%footer%'
# Перевод для action bar (verbose)
verbose:
layout:
# Макет для панели действий Sonar verbose
# Placeholders:
# - '%queued%' Количество игроков в очереди
# - '%verifying%' Количество проверяемых игроков
# - '%blacklisted%' Количество IP-адресов в черном списке
# - '%total-joins%' Количество всех попыток соединения
# - '%verify-total%' Количество попыток верификации
# - '%verify-success%' Количество проверенных IP-адресов
# - '%verify-failed%' Количество неудачных проверок
# - '%logins-per-second%' Количество входов на сервер в секунду
# - '%connections-per-second%' Количество соединений в секунду
# - '%attack-duration%' Продолжительность текущей атаки
# - '%incoming-traffic%' Использование входящего трафика в секунду
# - '%outgoing-traffic%' Использование исходящего трафика в секунду
# - '%incoming-traffic-ttl%' Общее использование входящего трафика пропускания
# - '%outgoing-traffic-ttl%' Общее использование исходящего трафика пропускания
# - '%used-memory%' Количество использованной памяти
# - '%total-memory%' Количество общей памяти
# - '%max-memory%' Объем максимальной памяти
# - '%free-memory%' Количество свободной памяти
# - '%animation%' Настраиваемый анимированный символ
# Перевод статистики в режиме реального времени (verbose)
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>На проверке <white>%verifying%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Макет action bar, отображающей информацию во время атаки
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>В очереди <white>%queued% <dark_aqua>╺
<gray>На проверке <white>%verifying% <dark_aqua>╺ <gray>Продолжительность <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Анимация для action bar (текст с названием предмета)
# Альтернативы:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# Перевод сообщений об атаках
notifications:
# Layout for Sonar's attack notification title
title: <yellow><b>Sonar<reset>
# Макет для подзаголовка уведомления об атаке
subtitle: <gray>Обнаружена бот-атака
# Макет для уведомления об атаке в чате
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>Бот-атака была обнаружена.
- ' <dark_aqua>▪ <gray>Количество входов в секунду: <white>%logins-per-second%'
- ' <dark_aqua>▪ <gray>Игроков проверяется: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Игроков в очереди: <white>%queued%'
- ''
- <gray>Просмотреть дополнительную информацию, используя <white><click:run_command:'/sonar
verbose'><hover:show_text:'(Нажмите чтобы выполнить команду)'>/sonar verbose</hover></click><gray>
или <white><click:run_command:'/sonar stats'><hover:show_text:'(Нажмите чтобы
выполнить команду)'>/sonar stats</hover></click><gray>.
- ''
@@ -0,0 +1,315 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Yardıma mı ihtiyacınız var veya sorularınız mı var? https://jonesdev.xyz/discord
# Kaynak kodu: https://github.com/jonesdevelopment/sonar
# Jones Development ve Sonar Katkıda Bulunanlar tarafından GPL-3.0 lisansı altında lisanslanmıştır
#
# Bu yapılandırma dosyasındaki her '%prefix%' için yer tutucu
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Bu yapılandırma dosyasındaki her '%support-url%' için yer tutucu
support-url: https://jonesdev.xyz/discord/
# Bu yapılandırma dosyasındaki her '%header%' için yer tutucu
header:
- <b><yellow>Sonar</b><reset>
- ''
# Bu yapılandırma dosyasındaki her '%footer%' için yer tutucu
footer:
- <gray>Bunun bir hata olduğunu düşünüyorsanız lütfen bir yönetici ile iletişime geçin.
# Birisi bağlandığında ancak IP adresleriyle çevrimiçi olan oyuncu sayısı fazla olduğunda gösterilen bağlantı kesme mesajı
too-many-online-per-ip:
- '%header%'
- <red>IP adresinizle çevrimiçi olan çok fazla oyuncu var.
- '%footer%'
# Komutlar için çeviriler
commands:
# Bir oyuncu /sonar komutunu izinsiz çalıştırdığında gösterilen mesaj
no-permission: '%prefix%<red>Bu komutu çalıştırmak için izniniz yok.'
# Birisi bir komutu yanlış kullandığında gösterilen mesaj
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
console-only: '%prefix%<red>Güvenlik nedeniyle, bu komutu sadece konsoldan çalıştırabilirsiniz.'
# Bir oyuncu Sonar komutlarını çok hızlı çalıştırdığında gösterilen mesaj
cool-down: '%prefix%<red>Bu komutu her 0,5 saniyede bir çalıştırabilirsiniz.'
cool-down-left: '%prefix%<red>Lütfen bir <bold>%time-left%sn<reset><red> daha bekleyin.'
# Bir oyuncunun belirli bir alt komutu çalıştırma izni olmadığında gösterilen mesaj
subcommand-no-permission: '%prefix%<red>Bu alt komutu çalıştırmak için izniniz yok. <gray>(%permission%)'
# '/sonar' için çeviriler
main:
# Ana komutu çalıştırırken her şeyin üstünde gösterilen bilgilendirici mesaj
header:
- <yellow><b>Sonar</b> %version% sürümü %platform% üzerinde çalışıyor.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
Jones Development ve Sonar Katkıda Bulunanlar</click>
- ''
- <gray>Yardıma mı ihtiyacınız var veya sorularınız mı var?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Discord\'u açmak için tıklayın)'>Discord üzerinden bir bilet açın </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(GitHub\'ı açmak için tıklayın)'>veya GitHub üzerinde yeni bir sorun açın.
- ''
# Ana komutu çalıştırırken gösterilen alt komutların listesi için biçimlendirme
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Sadece oyuncular: </gray>%only_players%<br><gray>Konsol gerekli: </gray>%require_console%<br><gray>İzin: </gray><white>%permission%<br><gray>Diğer isimler: </gray>%aliases%''><dark_aqua>
▪ <gray>/sonar %subcommand% <white>%description%'
# '/sonar reload' için çeviriler
reload:
# Birisi Sonar'ı yeniden yüklemeye başladığında gösterilen mesaj
start: '%prefix%Sonar yeniden yükleniyor...'
# Sonar yeniden yüklendiğinde gösterilen mesaj
finish: '%prefix%<green>Sonar başarıyla yeniden yüklendi <gray>(%taken%ms)'
# '/sonar verbose' için çeviriler
verbose:
# Bir oyuncu Sonar detaylı bilgilerini görmeye başladığında gösterilen mesaj
subscribed: '%prefix%Artık Sonar detaylı bilgilerini görüntülüyorsunuz.'
# Bir oyuncu Sonar detaylı bilgilerinden çıktığında gösterilen mesaj
unsubscribed: '%prefix%Artık Sonar detaylı bilgilerini görüntülemiyorsunuz.'
# '/sonar notify' için çeviriler
notify:
# Bir oyuncu Sonar saldırı bildirimlerini görmeye başladığında gösterilen mesaj
subscribed: '%prefix%Artık Sonar saldırı bildirimlerini görüntülüyorsunuz.'
# Bir oyuncu Sonar saldırı bildirimlerinden çıktığında gösterilen mesaj
unsubscribed: '%prefix%Artık Sonar saldırı bildirimlerini görüntülemiyorsunuz.'
# '/sonar blacklist' için çeviriler
blacklist:
# Birisi blacklist'i temizlemeye çalıştığında ama boş olduğunda gösterilen mesaj
empty: '%prefix%Kara liste şu anda boş. Bu nedenle, kara listeden hiçbir IP adresi kaldırılmadı.'
# Birisi kara listeyi temizlediğinde gösterilen mesaj
cleared: '%prefix%Toplamda %removed% IP adres(ler)i başarıyla kara listeden kaldırıldı.'
# Birisi kara listenin boyutunu kontrol ettiğinde gösterilen mesaj
size: '%prefix%Kara liste şu anda %amount% IP adres(ler)i içeriyor.'
# Birisi bir IP adresini kara listeye eklediğinde gösterilen mesaj
added: '%prefix%Başarıyla %ip% kara listeye eklendi.'
# Birisi doğrulanmış olarak işaretlenmiş bir IP adresini kara listeye eklediğinde gösterilen mesaj
added-warning: '%prefix%<red>Uyarı: <white>%ip% şu anda beyaz listede. Potansiyel sorunları önlemek için IP adresini doğrulanmış oyuncular listesinden kaldırmayı düşünün.'
# Birisi bir IP adresini kara listeden çıkardığında gösterilen mesaj
removed: '%prefix%Başarıyla %ip% kara listeden çıkarıldı.'
# Birisi bir IP adresini kara listeye eklemeye çalıştığında ama zaten kara listede olduğunda gösterilen mesaj
duplicate-ip: '%prefix%Sağladığınız IP adresi zaten kara listede.'
# Birisi bir IP adresini kara listeden çıkarmaya çalıştığında ama kara listede olmadığında gösterilen mesaj
ip-not-found: '%prefix%Sağladığınız IP adresi kara listede değil.'
# '/sonar verified' için çeviriler
verified:
# Birisi doğrulanmış oyuncular listesini temizlemeye çalıştığında ama boş olduğunda gösterilen mesaj
empty: '%prefix%Doğrulanmış oyuncular listesi şu anda boş. Bu nedenle, hiçbir oyuncu doğrulanmamış olarak işaretlenmedi.'
# Birisi doğrulanmış oyuncular listesini temizlediğinde gösterilen mesaj
cleared: '%prefix%Toplamda %removed% benzersiz oyuncu başarıyla doğrulanmamış olarak işaretlendi.'
# Birisi doğrulanmış oyuncular listesinin boyutunu kontrol ettiğinde gösterilen mesaj
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.'
# 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
blocked: '%prefix%Mevcut işlemin bitmesini bekleyin.'
# '/sonar statistics' için çeviriler
statistics:
# İstatistikleri görüntülerken her şeyin üzerinde gösterilen bilgilendirme mesajı
header: 'Bu oturum için %type% istatistikleri gösteriliyor:'
# Message that is shown when a player tries viewing an unknown statistic
unknown-type: '%prefix%<red>Bilinmeyen istatistik türü! Mevcut istatistikler: <gray>%statistics%'
# Genel istatistik mesajının biçimi
general:
- ' <dark_aqua>▪ <gray>Doğrulanmış IP adresleri: <white>%verified%'
- ' <dark_aqua>▪ <gray>Doğrulanan IP adresleri: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Kara listedeki IP adresleri: <white>%blacklisted%'
- ' <dark_aqua>▪ <gray>Şu anda sıraya alınmış girişler: <white>%queued%'
- ' <dark_aqua>▪ <gray>Toplam benzersiz olmayan girişler: <white>%total_joins%'
- ' <dark_aqua>▪ <gray>Toplam doğrulama denemeleri: <white>%total_attempts%'
- ' <dark_aqua>▪ <gray>Toplam başarısız doğrulama denemesi: <white>%total_failed%'
- ' <dark_aqua>▪ <gray>Sunucu çalışma süresi: <white>%uptime%'
# İşlemci istatistik mesajının biçimi
cpu:
- ' <dark_aqua>▪ <gray>Şu anda işlemci CPU kullanımı: <white>%process_cpu%%'
- ' <dark_aqua>▪ <gray>Şu anda sistem CPU kullanımı: <white>%system_cpu%%'
- ' <dark_aqua>▪ <gray>Çekirdek başına işlem CPU kullanımı: <white>%average_process_cpu%%'
- ' <dark_aqua>▪ <gray>Çekirdek başına sistem CPU kullanımı: <white>%average_system_cpu%%'
- ' <dark_aqua>▪ <gray>Genel sistem yük ortalaması: <white>%load_average%%'
- ' <dark_aqua>▪ <gray>Toplam sanal CPU miktarı: <white>%virtual_cores%'
# Hafıza istatistik mesajının biçimi
memory:
- ' <dark_aqua>▪ <gray>Toplam boş hafıza: <white>%free_memory%'
- ' <dark_aqua>▪ <gray>Toplam kullanılan hafıza: <white>%used_memory%'
- ' <dark_aqua>▪ <gray>Toplam maksimum bellek: <white>%max_memory%'
- ' <dark_aqua>▪ <gray>Toplam ayrılmış bellek: <white>%total_memory%'
# İnternet istatistik mesajının biçimi
network:
- ' <dark_aqua>▪ <gray>Mevcut gelen kullanılan bant genişliği: <white>%incoming%'
- ' <dark_aqua>▪ <gray>Mevcut giden kullanılan bant genişliği: <white>%outgoing%'
- ' <dark_aqua>▪ <gray>Toplam gelen kullanılan bant genişliği: <white>%ttl_incoming%'
- ' <dark_aqua>▪ <gray>Toplam giden kullanılan bant genişliği: <white>%ttl_outgoing%'
# Doğrulama işlemi sırasında tüm mesajlar için çeviriler
verification:
# Doğrulama sırasında tüm hata ayıklama mesajları için çeviriler
logs:
# Sunucuya yeni bir oyuncu katıldığında konsola kaydedilen mesaj
connection: '%name%%ip% (%protocol%) sunucuya bağlandı.'
# Bir oyuncu doğrulamada başarısız olduğunda konsola kaydedilen mesaj
failed: '%ip% (%protocol%) bot korumasında başarısız oldu: %reason%'
# Bir oyuncu kara listeye alındığında konsola kaydedilen mesaj
blacklisted: '%ip% (%protocol%) çok fazla doğrulama denemesinden dolayı kara listeye alındı'
# Bir oyuncu doğrulandığında konsola kaydedilen mesaj
successful: '%name% başarıyla doğrulandı! (%time%s).'
# Yerçekimi için kontrol edildiklerinde oyuncuya gösterilen mesaj
welcome: '%prefix%<gray>Lütfen doğrulamanın bitmesini bekleyin...'
# CAPTCHA'yı girmeleri gerektiğinde oyuncuya gösterilen mesajlar
captcha:
# Captcha'nın cevabını girmeleri gerektiğinde oyuncuya gösterilen mesaj
enter-code: '%prefix%<green>Haritada gösterilen kodu lütfen sohbete girin.'
# Timer that is shown to the player when they have to enter the answer to the captcha
# (Set this to '' to disable the action bar message)
action-bar: '%prefix%<green>Kodu girmek için %time-left% saniyeniz kaldı'
# Message that is shown to the player when they enter the wrong answer in chat
incorrect: '%prefix%<red>Yanlış kodu girdiniz. Lütfen tekrar deneyin.'
# Captcha henüz hazırlanmamışken birisi katıldığında gösterilen bağlantıyı kesme mesajı
currently-preparing:
- '%header%'
- <yellow>Anti-bot verileriniz henüz hazır değil.
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
- '%footer%'
# Aynı anda çok fazla oyuncu doğrulama yaptığında gösterilen bağlantıyı kesme mesajı
too-many-players:
- '%header%'
- <gold>Too many players are currently trying to log in, try again later.
- <gray>Please wait a few seconds before trying to join again.
- '%footer%'
# Doğrulama sırasında birisi çok hızlı bir şekilde yeniden katıldığında gösterilen bağlantıyı kesme mesajı
too-fast-reconnect:
- '%header%'
- <gold>Çok hızlı yeniden bağlandın.
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
- '%footer%'
# Birisi katıldığında ancak zaten doğrulama yaparken gösterilen bağlantıyı kesme mesajı
already-verifying:
- '%header%'
- <red>IP adresiniz şu anda doğrulanıyorç
- <red>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
- '%footer%'
# Birisi katıldığında gösterilen ancak doğrulama için zaten sıraya alınmış olduğunda gösterilen bağlantıyı kesme mesajı
already-queued:
- '%header%'
- <red>IP adresiniz şu an doğrulanma sırasında.
- <red>Tekrar doğrulamayı denemeden önce lütfen birkaç dakika bekleyin.
- '%footer%'
# Birisi katıldığında ancak geçici olarak kara listeye alındığında gösterilen bağlantıyı kesme mesajı
blacklisted:
- '%header%'
- <red>Şu anda sunucuya girişiniz engellenmiştir.
- <red>Sunucuya tekrar katılabilmek için lütfen birkaç dakika bekleyin.
- <gold>Bunun bir hata olduğunu mu düşünüyorsunuz? <gray>%support-url%
- '%footer%'
# Birisi geçersiz bir kullanıcı adıyla katıldığında gösterilen bağlantıyı kesme mesajı
invalid-username:
- '%header%'
- <red>Kullanıcı adınız geçersiz karakterler içeriyor.
- '%footer%'
# Birisi kara listeye alınmış bir sürümle katıldığında gösterilen bağlantıyı kes mesajı
blacklisted-protocol:
- '%header%'
- <red>Sunucumuzda izin verilmeyen bir sürüm ile giriş yapmaya çalışıyorsun.
- <gold>Giriş yapma hakkında yardıma mı ihtiyacın var? <gray>%support-url%
- '%footer%'
# Birisi çevrimiçi olan bir hesapla doğrulama yapmaya çalıştığında gösterilen bağlantıyı kesme mesajı
already-online:
- '%header%'
- <red>Hesabınızla zaten çevrimiçi olan biri var.
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
- <gray>Bu durum devam ederse, oyununuzu yeniden başlatmayı deneyin veya destek ekibiyle iletişime geçin.
- '%footer%'
# Birisi başarıyla doğrulama yaptığında gösterilen bağlantıyı kesme mesajı
success:
- '%header%'
- <green>Doğrulamayı başarıyla geçtin.
- <white>Yeniden bağlandığınızda artık sunucuda oynayabilirsiniz.
# Birisi doğrulamayı başaramadığında gösterilen bağlantıyı kesme mesajı
failed:
- '%header%'
- <red>Doğrulamayı geçemediniz.
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
- <gold>Giriş yapma hakkında yardıma mı ihtiyacın var? <gray>%support-url%
- '%footer%'
# Sonar'ın actionbar verbose mesajları için çeviriler
verbose:
layout:
# Sonar verbose için yer tutucular
# Placeholders:
# - '%queued%' Sırada bekleyen bağlantı sayısı
# - '%verifying%' Doğrulanıyor olan bağlantı sayısı
# - '%blacklisted%' Kara listeye alınmış IP adresi sayısı
# - '%total-joins%' Toplam denenen katılım sayısı
# - '%verify-total%' Toplam doğrulama denemesi sayısı
# - '%verify-success%' Doğrulanmış IP adresi sayısı
# - '%verify-failed%' Başarısız doğrulama sayısı
# - '%logins-per-second%' Saniye başına giriş sayısı
# - '%connections-per-second%' Saniye başına bağlantı sayısı
# - '%attack-duration%' Şu anki bağlantının süresi
# - '%incoming-traffic%': Saniyedeki gelen bant genişliği kullanımı
# - '%outgoing-traffic%': Saniyedeki giden bant genişliği kullanımı
# - '%incoming-traffic-ttl%': Toplam gelen bant genişliği kullanımı
# - '%outgoing-traffic-ttl%': Toplam giden bant genişliği kullanımı
# - '%used-memory%' İşlemin kullanılan bellek miktarı
# - '%total-memory%' İşlemin toplam bellek miktarı
# - '%max-memory%' İşlemin maksimum bellek miktarı
# - '%free-memory%' İşlemin boş bellek miktarı
# - '%animation%' Özelleştirilebilir animasyonlu simge
# Translations for Sonar's normal verbose output
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Girişler/sn
<white>%logins-per-second% <dark_aqua>╺ <gray>Doğrulama <white>%verifying% <dark_aqua>|
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Layout for Sonar's actionbar verbose during an attack
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Girişler/sn
<white>%logins-per-second% <dark_aqua>╺ <gray>Sıra <white>%queued% <dark_aqua>╺
<gray>Doğrulama <white>%verifying% <dark_aqua>╺ <gray>Saldırı süresi <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Animation for the action bar
# Alternatives:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# Translations for Sonar's attack notification messages
notifications:
# Layout for Sonar's attack notification title
title: <yellow><b>Sonar<reset>
# Layout for Sonar's attack notification subtitle
subtitle: <gray>Bir bot saldırısı tespit edildi
# Layout for Sonar's attack notification in chat
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>Bir bot saldırısı tespit edildi.
- ' <dark_aqua>▪ <gray>Saniye başına giriş sayısı: <white>%logins-per-second%'
- ' <dark_aqua>▪ <gray>Doğrulanıyor olan oyuncular: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Sırada bekleyen oyuncular: <white>%queued%'
- ''
- <gray>Daha fazla bilgi görmek için <white><click:run_command:'/sonar verbose'><hover:show_text:'(Çalıştırmak için tıkla)'>/sonar verbose</hover></click><gray> veya <white><click:run_command:'/sonar
stats'><hover:show_text:'(Çalıştırmak için tıkla)'>/sonar stats</hover></click><gray> kullanın.
- ''
@@ -0,0 +1,60 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Potřebujete pomoc nebo máte otázky? https://jonesdev.xyz/discord
# Zdrojový kód: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development a Sonar Contributors, licencováno pod GPL-3.0
#
# Notifikace útoků můžou být poslány na discord server pomocí webhooků
webhook:
# URL Discord webhooku (Ponechejte '' pro žádné webhooky)
url: ''
# Jméno discord webhooku
username: Sonar
# URL pro avatara discord webhooku (Ponechejte na '' pro žádného)
avatar-url: ''
# Obsah zprávy webhooku (Ponechte na '' pro žadnou)
# Můžete to použít k např. upozorvování členů staff týmu pomocí <@userId>
# Pokuď budete chtít upozornit role, budete muset použít <@&roleId>
content: ''
# Přidejte zprávu pro webhook, kterou ma poslat když skončil útok
embed:
# Malé zápatí pro poslanou zprávu webhookem
footer:
# Obsah zápatí kterou discord webhooku
text: © Jones Development and Sonar Contributors
# URL ikony zápatí zprávy
icon-url: ''
# Nadpis discord webhook zprávy
title: ':white_check_mark: Útok zahájen'
# URL, na kterou se bude moct na webhook zprávě kliknout
title-url: ''
# Popisek (kontext) discord webhook zprávy
description:
- Útok na váš server byl ukončen!.
- ''
- 'Začátek útoku: <t:%start-timestamp%:T>'
- 'Konec útoku: <t:%end-timestamp%:T>'
- 'Délka útoku: %duration%'
- ''
- 'Nejvyšší využití CPU při útoku: %peak-cpu%%'
- 'Nejvyšší využití paměti při útoku: %peak-memory%'
- 'Nejvyšší počet botů za sekundu při útoku: %peak-bps%'
- ''
- 'IP přidány na černou listinu: %total-blacklisted%'
- 'Nepovedené verifikace při útoku: %total-failed%'
- 'Povedené verifikace při útoku: %total-success%'
# RGB barvy discord webhook zprávy
# Barvy: https://www.rapidtables.com/web/color/RGB_Color.html
color:
red: 0
green: 255
blue: 0
@@ -0,0 +1,60 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Need help or have questions? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Bot attack notifications can also be sent to your Discord server using webhooks
webhook:
# URL of the Discord webhook (Set this to '' to disable webhooks)
url: ''
# Username of the Discord webhook sender
username: Sonar
# URL to the avatar of the Discord webhook sender (Set this to '' to disable)
avatar-url: ''
# Content of the Discord webhook message (Set this to '' to disable)
# You can use this to e.g. ping staff members using <@userId>
# If you want to ping roles, you will need to use <@&roleId>
content: ''
# Embed Discord webhook message that is sent when an attack has stopped
embed:
# Small footer message of the Discord webhook embed
footer:
# Content of the footer message of the Discord webhook embed
text: © Jones Development and Sonar Contributors
# URL of the footer message icon of the Discord webhook embed
icon-url: ''
# Title of the Discord webhook embed
title: ':white_check_mark: Attack mitigated'
# Clickable URL of the title of the Discord webhook embed
title-url: ''
# Description (content) of the Discord webhook embed
description:
- The attack on your server has been mitigated.
- ''
- 'Attack start: <t:%start-timestamp%:T>'
- 'Attack end: <t:%end-timestamp%:T>'
- 'Attack duration: %duration%'
- ''
- 'Peak process CPU usage during the attack: %peak-cpu%%'
- 'Peak process memory usage during the attack: %peak-memory%'
- 'Peak bots per second during the attack: %peak-bps%'
- ''
- 'Blacklisted IP addresses during the attack: %total-blacklisted%'
- 'Failed verifications during the attack: %total-failed%'
- 'Successful verifications during the attack: %total-success%'
# RGB colors of the Discord webhook embed
# Color picker: https://www.rapidtables.com/web/color/RGB_Color.html
color:
red: 0
green: 255
blue: 0
@@ -0,0 +1,60 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# დახმარება გჭირდება ან კითხვა გაქვს? https://jonesdev.xyz/discord
# საწყისი კოდი: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# შემოტევასთან დაკავშირებული მესიჯების გაგზავნა თქვენს Discord სერვერში შეიძლება ვებჰუკების მეშვეობით
webhook:
# ვებჰუკის ლინკი (დააყენეთ ეს '' თუ გინდათ, რომ გათიშოთ ეს ფუნქცია)
url: ''
# სახელი ვებჰუკის გამგზავნელის
username: Sonar
# ლინკი ავატარის, რომელიც იქნება ვებჰუკის ავატარად გამოყენებული (დააყენეთ ეს '' თუ გინდათ, რომ გათიშოთ ეს ფუნქცია)
avatar-url: ''
# ვებჰუკის შეტყობინების შინაარსი (დააყენეთ ეს '' თუ გინდათ, რომ გათიშოთ ეს ფუნქცია)
# თქვენ შეგიძლიათ ეს გამოიყენოთ, რომ მაგალითად მოთაგოთ სტაფის წევრი <@userId>
# თუ გინდათ, რომ მოთაგოთ როლი, გამოიყენეთ ეს <@&roleId>
content: ''
# განათავსეთ ვებჰუკის შეტყობინება, რომელიც იგზავნება შეტევის შეჩერებისას
embed:
# ვებჰუკის შეტყობინების ქვედა მცირე ტექსტი
footer:
# ვებჰუკის ქვედა მცირე შეტყობინების შინაარსი
text: © Jones Development and Sonar Contributors
# ლინკი ქვედა მცირე შეტყობინობის ავატარისთვის
icon-url: ''
# ვებჰუკის სათაური
title: ':white_check_mark: შეტევა გადატანილია'
# ლინკი რომელზეც გადავნენ ვებჰუკის სათაურზე დაჭერის შემდეგ
title-url: ''
# აღწერა (ტექსტი) ვებჰუკის მესიჯის
description:
- თქვენს სერვზე განხორციელებული შემოტევა წარმატებულად იყო გადატანილი.
- ''
- 'შემოტევა დაიწყო: <t:%start-timestamp%:T>'
- 'შემოტევა დასრულდა: <t:%end-timestamp%:T>'
- 'შემოტევის ხანგრძლივობა: %duration%'
- ''
- 'CPU-ის გამოყენების პიკი შეტევის დროს: %peak-cpu%%'
- 'RAM-ის გამოყენების პიკი შეტევის დროს: %peak-memory%'
- 'ბოტების პიკი წამში შეტევის დროს: %peak-bps%'
- ''
- 'შეტევის დროს შავ სიაში IP მისამართები: %total-blacklisted%'
- 'შეტევის დროს ვერიფიკაცია ვერ შეასრულა: %total-failed%'
- 'წარმატებულად ვერიფირებული თავდასხმის დროს: %total-success%'
# RGB ფერები ვებჰუკის მესიჯისთვის
# Color picker: https://www.rapidtables.com/web/color/RGB_Color.html
color:
red: 0
green: 255
blue: 0
@@ -0,0 +1,60 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
# Исходный код: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Уведомления о бот-атаках могут быть отправлены на ваш сервер Discord с помощью вебхуков
webhook:
# URL вебхука Discord (Установите значение '' чтобы отключить вебхук)
url: ''
# Ник вебхука Discord
username: Sonar
# URL аватара вебхука Discord (Установите значение '' чтобы не использовать аватар)
avatar-url: ''
# Контент сообщения вебхука Discord (Установите значение '' чтобы не использовать контент)
# Вы можете использовать это для оповещения персонала, например: <@userId>
# Если вы хотите пинговать роли, вам нужно использовать <@&roleId>
content: ''
# Embed сообщение, отправляемое после прекращения атаки
embed:
# Нижний текст в панельном сообщении
footer:
# Содержимое нижнего текста в панельном сообщении
text: © Jones Development and Sonar Contributors
# Иконка нижнего текста в панельном сообщении
icon-url: ''
# Заголовок панельного сообщения
title: ':white_check_mark: Attack mitigated'
# Кликабельный URL-адрес заголовка
title-url: ''
# Описание (контент) панельного сообщения
description:
- Атака на ваш сервер была устранена.
- ''
- 'Начало атаки: <t:%start-timestamp%:T>'
- 'Конец атаки: <t:%end-timestamp%:T>'
- 'Продолжительность атаки: %duration%'
- ''
- 'Пиковое использование процессора во время атаки: %peak-cpu%%'
- 'Пиковое использование памяти во время атаки: %peak-memory%'
- 'Пиковая скорость входа ботов в секунду во время атаки: %peak-bps%'
- ''
- 'IP-адреса, внесенные в черный список во время атаки: %total-blacklisted%'
- 'Проваленные проверки во время атаки: %total-failed%'
- 'Успешные проверки во время атаки: %total-success%'
# Цвета RGB используемые в панельном сообщении
# Выбор цвета: https://www.rapidtables.com/web/color/RGB_Color.html
color:
red: 0
green: 255
blue: 0
@@ -22,12 +22,13 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.velocity.audience.AudienceListener;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.velocity.fallback.FallbackLoginListener;
import java.util.concurrent.TimeUnit;
@@ -76,13 +77,13 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackListener());
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackLoginListener());
// Register audience register listener
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
// Register traffic service
getPlugin().getServer().getScheduler().buildTask(getPlugin(), TrafficCounter::reset)
getPlugin().getServer().getScheduler().buildTask(getPlugin(), Bandwidth::reset)
.repeat(1L, TimeUnit.SECONDS)
.schedule();
@@ -95,5 +96,8 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
getPlugin().getServer().getScheduler().buildTask(getPlugin(), Sonar.get().getVerboseHandler()::update)
.repeat(200L, TimeUnit.MILLISECONDS)
.schedule();
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject(getPlugin().getServer());
}
}
@@ -0,0 +1,234 @@
/*
* 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.util.GameProfile;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
import java.net.InetAddress;
import java.net.InetSocketAddress;
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.closeWith;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private final Channel channel;
private @Nullable String username;
private InetAddress inetAddress;
private ProtocolVersion protocolVersion;
private HandshakePacket handshakePacket;
private static final Fallback FALLBACK = Sonar.get().getFallback();
@Override
public void channelActive(final @NotNull ChannelHandlerContext ctx) {
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) {
// The player can disconnect without sending the login packet first
if (username != null) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
}
// The player cannot be in the queue if the IP address is invalid
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// Intercept any handshake packet by the client
if (msg instanceof HandshakePacket handshake) {
handleHandshake(handshake);
}
// Intercept any server login packet by the client
if (msg instanceof ServerLoginPacket serverLogin) {
handleLogin(ctx, serverLogin);
return;
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
// We can override the default exceptionCaught method since this handler
// will run before the MinecraftConnection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Sonar sometimes uses exceptions to quickly close
// the channel and interrupt any other ongoing process.
// Simply close the channel if we encounter any errors.
ctx.close();
}
private void handleHandshake(final @NotNull HandshakePacket handshake) throws Exception {
// Check if the player has already sent a handshake packet
if (handshakePacket != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Check if the hostname is invalid
if (handshake.getServerAddress().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion().getProtocol());
// Store the handshake packet
handshakePacket = handshake;
// Hook the traffic listener
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull ServerLoginPacket serverLogin) throws Exception {
// Check if the player has already sent a login packet
if (username != null) {
throw new CorruptedFrameException("Already logged on");
}
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Store the username
username = serverLogin.getUsername();
// Make sure to use the potentially modified, original IP
final MinecraftConnection minecraftConnection = (MinecraftConnection) channel.pipeline().get(HANDLER);
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getBlacklisted(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(serverLogin);
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(serverLogin);
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(serverLogin.getUsername())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
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())) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getProtocolBlacklisted(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
return;
}
// Make sure we actually have to verify the player
final GameProfile gameProfile = GameProfile.forOfflinePlayer(serverLogin.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) {
ctx.fireChannelRead(serverLogin);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
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);
return;
}
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(gameProfile.getName()).matches()) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getInvalidUsername(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
// Let the verification handler take over the channel
user.hijack(
gameProfile.getName(), gameProfile.getId(), MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
}));
}
}
@@ -0,0 +1,56 @@
/*
* 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 io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import java.lang.reflect.Method;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
@RequiredArgsConstructor
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
private static final Method INIT_CHANNEL_METHOD;
static {
try {
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
INIT_CHANNEL_METHOD.setAccessible(true);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
private final ChannelInitializer<Channel> originalChannelInitializer;
@Override
protected void initChannel(final Channel channel) throws Exception {
// Invoke the original method
try {
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
channel.pipeline().addAfter(MINECRAFT_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
}
}
@@ -1,248 +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.proxy.connection.MinecraftConnection;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress;
public final class FallbackClosedConnection extends MinecraftConnection {
public FallbackClosedConnection() {
super(new Channel() {
@Override
public ChannelId id() {
return null;
}
@Override
public EventLoop eventLoop() {
return null;
}
@Override
public Channel parent() {
return null;
}
@Override
public ChannelConfig config() {
return null;
}
@Override
public boolean isOpen() {
return false;
}
@Override
public boolean isRegistered() {
return false;
}
@Override
public boolean isActive() {
return false;
}
@Override
public ChannelMetadata metadata() {
return null;
}
@Override
public SocketAddress localAddress() {
return null;
}
@Override
public SocketAddress remoteAddress() {
return null;
}
@Override
public ChannelFuture closeFuture() {
return null;
}
@Override
public boolean isWritable() {
return false;
}
@Override
public long bytesBeforeUnwritable() {
return 0;
}
@Override
public long bytesBeforeWritable() {
return 0;
}
@Override
public Unsafe unsafe() {
return null;
}
@Override
public ChannelPipeline pipeline() {
return null;
}
@Override
public ByteBufAllocator alloc() {
return null;
}
@Override
public Channel read() {
return null;
}
@Override
public Channel flush() {
return null;
}
@Override
public ChannelFuture bind(SocketAddress socketAddress) {
return null;
}
@Override
public ChannelFuture connect(SocketAddress socketAddress) {
return null;
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1) {
return null;
}
@Override
public ChannelFuture disconnect() {
return null;
}
@Override
public ChannelFuture close() {
return null;
}
@Override
public ChannelFuture deregister() {
return null;
}
@Override
public ChannelFuture bind(SocketAddress socketAddress, ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1, ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture disconnect(ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture close(ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture deregister(ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture write(Object o) {
return null;
}
@Override
public ChannelFuture write(Object o, ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture writeAndFlush(Object o, ChannelPromise channelPromise) {
return null;
}
@Override
public ChannelFuture writeAndFlush(Object o) {
return null;
}
@Override
public ChannelPromise newPromise() {
return null;
}
@Override
public ChannelProgressivePromise newProgressivePromise() {
return null;
}
@Override
public ChannelFuture newSucceededFuture() {
return null;
}
@Override
public ChannelFuture newFailedFuture(Throwable throwable) {
return null;
}
@Override
public ChannelPromise voidPromise() {
return null;
}
@Override
public <T> Attribute<T> attr(AttributeKey<T> attributeKey) {
return null;
}
@Override
public <T> boolean hasAttr(AttributeKey<T> attributeKey) {
return false;
}
@Override
public int compareTo(@NotNull Channel o) {
return 0;
}
}, null);
}
}
@@ -0,0 +1,62 @@
/*
* 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.proxy.ProxyServer;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.network.ServerChannelInitializerHolder;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import java.lang.reflect.Field;
@UtilityClass
public class FallbackInjectionHelper {
private final Field CONNECTION_MANAGER_FIELD;
private final Field SERVER_CHANNEL_INITIALIZER_FIELD;
static {
try {
CONNECTION_MANAGER_FIELD = VelocityServer.class.getDeclaredField("cm");
CONNECTION_MANAGER_FIELD.setAccessible(true);
SERVER_CHANNEL_INITIALIZER_FIELD = ServerChannelInitializerHolder.class.getDeclaredField("initializer");
SERVER_CHANNEL_INITIALIZER_FIELD.setAccessible(true);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
public void inject(final ProxyServer proxyServer) {
try {
final ConnectionManager connectionManager = (ConnectionManager) CONNECTION_MANAGER_FIELD.get(proxyServer);
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = connectionManager.serverChannelInitializer.get();
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
// Replace the original channel initializer
SERVER_CHANNEL_INITIALIZER_FIELD.set(connectionManager.getServerChannelInitializer(), fallbackInitializer);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
}
@@ -1,309 +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.ConnectionHandshakeEvent;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.client.InitialInboundConnection;
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
import com.velocitypowered.proxy.connection.client.LoginInboundConnection;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import xyz.jonesdev.sonar.velocity.SonarVelocity;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
public final class FallbackListener {
private static final Fallback FALLBACK = Sonar.get().getFallback();
private static final FallbackClosedConnection CLOSED_CONNECTION;
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodHandle INITIAL_CONNECTION;
private static final Field CONNECTION_FIELD;
static {
CLOSED_CONNECTION = new FallbackClosedConnection();
try {
CONNECTION_FIELD = InitialLoginSessionHandler.class.getDeclaredField("mcConnection");
CONNECTION_FIELD.setAccessible(true);
INITIAL_CONNECTION = MethodHandles.privateLookupIn(LoginInboundConnection.class, LOOKUP)
.findGetter(LoginInboundConnection.class,
"delegate",
InitialInboundConnection.class);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
private static void markConnectionAsDead(final MinecraftSessionHandler sessionHandler) throws Throwable {
// The AuthSessionHandler isn't supposed to continue the connection process,
// which is why we override the field value for the MinecraftConnection with
// a fake (closed) connection.
CONNECTION_FIELD.set(sessionHandler, CLOSED_CONNECTION);
}
@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());
}
@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());
final LoginInboundConnection inboundConnection = (LoginInboundConnection) event.getConnection();
val initialConnection = (InitialInboundConnection) INITIAL_CONNECTION.invokeExact(inboundConnection);
final MinecraftConnection mcConnection = initialConnection.getConnection();
final Channel channel = mcConnection.getChannel();
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
try {
val activeSessionHandler = mcConnection.getActiveSessionHandler();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress)) {
// Mark the connection as dead to avoid unnecessary console logs
markConnectionAsDead(activeSessionHandler);
mcConnection.closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getBlacklisted(),
mcConnection.getProtocolVersion(), true));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) return;
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
event.getUsername(), Sonar.get().getConfig().formatAddress(inetAddress));
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
final int protocolId = mcConnection.getProtocolVersion().getProtocol();
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
// Mark the connection as dead to avoid unnecessary console logs
markConnectionAsDead(activeSessionHandler);
mcConnection.closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getProtocolBlacklisted(),
mcConnection.getProtocolVersion(), true));
return;
}
// Check if the player is already verified.
// No one wants to be verified over and over again.
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) return;
// We now mark the connection as dead by using our fake connection
markConnectionAsDead(activeSessionHandler);
// Don't allow exceptions or disconnect messages
mcConnection.setAssociation(null);
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
mcConnection.closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
mcConnection.getProtocolVersion(), true));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(event.getUsername())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
mcConnection.closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
mcConnection.getProtocolVersion(), true));
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
mcConnection.closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
mcConnection.getProtocolVersion(), true));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername(), inetAddress));
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(event.getUsername()).matches()) {
mcConnection.closeWith(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(),
mcConnection.getProtocolVersion(), true));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
pipeline.replace(READ_TIMEOUT, READ_TIMEOUT, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// Create an instance for the Fallback connection
final FallbackUserWrapper user = new FallbackUserWrapper(
FALLBACK, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(protocolId));
// Disconnect if the protocol version could not be resolved
if (user.getProtocolVersion().isUnknown()) {
mcConnection.closeWith(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidProtocol(),
mcConnection.getProtocolVersion(), true));
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", gameProfile.getName())
.replace("%ip%", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), user));
// Mark the player as connected → verifying players
FALLBACK.getConnected().put(gameProfile.getName(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (user.getPipeline().get(MINECRAFT_ENCODER) == null
|| user.getPipeline().get(MINECRAFT_DECODER) == null) {
mcConnection.close(true);
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
user.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
user.write(new LoginSuccess(gameProfile.getName(), gameProfile.getId()));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
user.getPipeline().replace(MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
new FallbackVerificationHandler(user, gameProfile.getName(), gameProfile.getId())));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull LoginEvent event) {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
if (maxOnlinePerIp > 0) {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) event.getPlayer();
final InetAddress inetAddress = event.getPlayer().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) {
connectedPlayer.getConnection().closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getTooManyOnlinePerIp(), connectedPlayer.getProtocolVersion(), true));
}
}
}
}
@@ -0,0 +1,57 @@
/*
* 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.LoginEvent;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
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 LoginEvent 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.getPlayer().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) {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) event.getPlayer();
connectedPlayer.getConnection().closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getTooManyOnlinePerIp(),
connectedPlayer.getProtocolVersion(), StateRegistry.LOGIN));
}
}
}
@@ -1,56 +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.proxy.connection.MinecraftConnection;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
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.packets.play.Disconnect;
import java.net.InetAddress;
@Getter
@RequiredArgsConstructor
@ToString(of = {"protocolVersion"})
public final class FallbackUserWrapper implements FallbackUser<MinecraftConnection> {
private final Fallback fallback;
private final MinecraftConnection connection;
private final Channel channel;
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
/**
* Disconnect the player during/after verification
* using our custom {@link Disconnect} packet.
*
* @param reason Disconnect message component
*/
@Override
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
}
@@ -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
}
],