Skip to content

Commit

Permalink
Recode some of the verification and improve networking (#96)
Browse files Browse the repository at this point in the history
* feat: implement block state id from PrismarineJS/minecraft-data#786

* feat: recode dimension nbt codec for JoinGame packet

* feat(fallback): recode gravity/block collisions check

* fix: remove unnecessary debug message

* fix: dynamicSpawnYPosition using a set Y buffer

* feat: refactor some verification-related configuration values

* fix: forEnumConstant() not saving default config values

* fix: remove Geyser characters from valid name regex

* feat: apply migration fix for every config string

* fix: don't do .toUpperCase() for every string

* feat: add more description to some enum config types

* fix: move "recommended" to listing in comment

* fix: remove mc association before queuing the connection

* refactor: closed -> knownDisconnect

* fix: incompatibility with latest BungeeCord

* refactor: fallbackPlayer -> user

* fix: BungeeCord kick message serialization not working
  • Loading branch information
jonesdevelopment authored Nov 4, 2023
1 parent 4ae14c5 commit 0efc6ae
Show file tree
Hide file tree
Showing 27 changed files with 14,789 additions and 488 deletions.
2 changes: 1 addition & 1 deletion .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,5 @@ Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org
## Credits

- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
- The nbt mappings were taken from [LimboAPI](https://github.com/Elytrium/LimboAPI).
- The dimension codecs were taken from [NanoLimbo](https://github.com/Nan1t/NanoLimbo).
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,18 @@ public boolean getBoolean(final String path, final boolean def) {
}

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);
set(path, def);
return def;
}

public Object getObject(final String path, final Object def) {
yaml.addDefault(path, def);
return yaml.getString(path, def);
return yaml.get(path, def);
}

public List<String> getStringList(final String path, final List<String> def) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ public enum Timing {
}

private boolean checkGravity;
private boolean checkCollisions;
private boolean logConnections;
private boolean logDuringAttack;
private boolean debugXYZPositions;
Expand All @@ -118,7 +117,20 @@ public enum Timing {
private String failedLog;
private String successLog;
private String blacklistLog;
private short gamemodeId;

private Gamemode gamemode;

@Getter
@RequiredArgsConstructor
public enum Gamemode {
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2),
SPECTATOR(3);

private final int id;
}

private int maxBrandLength;
private int maxMovementTicks;
private int maxIgnoredTicks;
Expand Down Expand Up @@ -327,8 +339,8 @@ public void load() {
generalConfig.getYaml().setComment("database.type",
"Type of database Sonar uses to store verified players"
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL");
final String newDatabaseType = generalConfig.getString("database.type", Database.Type.NONE.name());
database.type = Database.Type.valueOf(newDatabaseType.toUpperCase());
database.type = Database.Type.valueOf(
generalConfig.getString("database.type", Database.Type.NONE.name()).toUpperCase());

generalConfig.getYaml().setComment("database",
"You can connect Sonar to a database to keep verified players even after restarting your server"
Expand Down Expand Up @@ -390,8 +402,11 @@ public void load() {
"Every new player that joins for the first time will be sent to"
+ LINE_SEPARATOR + "a lightweight limbo server where advanced bot checks are performed");
generalConfig.getYaml().setComment("verification.timing",
"When should Sonar verify new players? (Recommended: ALWAYS)"
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER");
"When should Sonar verify new players?"
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER"
+ LINE_SEPARATOR + "- ALWAYS: New players will always be checked (Recommended)"
+ LINE_SEPARATOR + "- DURING_ATTACK: New players will only be checked during an attack"
+ LINE_SEPARATOR + "- NEVER: New players will never be checked");
verification.timing = Verification.Timing.valueOf(
generalConfig.getString("verification.timing", Verification.Timing.ALWAYS.name()).toUpperCase());

Expand All @@ -412,16 +427,42 @@ public void load() {
verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1,
128);

generalConfig.getYaml().setComment("verification.checks.collisions",
"Checks if the players collides with barrier blocks spawned below the player"
+ LINE_SEPARATOR + "Note: The collision check will be skipped if the gravity check is disabled");
generalConfig.getYaml().setComment("verification.checks.collisions.enabled",
"Should Sonar check for valid client collisions? (Recommended)");
verification.checkCollisions = generalConfig.getBoolean("verification.checks.collisions.enabled", true);
generalConfig.getYaml().setComment("verification.checks.valid-name-regex",
"Regex for validating usernames during verification");
verification.validNameRegex = Pattern.compile(generalConfig.getString(
"verification.checks.valid-name-regex", "^[a-zA-Z0-9_]+$"));

generalConfig.getYaml().setComment("verification.checks.valid-brand-regex",
"Regex for validating client brands during verification");
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
"verification.checks.valid-brand-regex", "^[!-~ ]+$"));

generalConfig.getYaml().setComment("verification.checks.valid-locale-regex",
"Regex for validating client locale during verification");
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
"verification.checks.valid-locale-regex", "^[a-zA-Z_]+$"));

generalConfig.getYaml().setComment("verification.checks.max-brand-length",
"Maximum client brand length during verification");
verification.maxBrandLength = generalConfig.getInt("verification.checks.max-brand-length", 64);

generalConfig.getYaml().setComment("verification.checks.max-ping",
"Ping (in milliseconds) a player has to have in order to timeout");
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping", 10000), 500, 30000);

generalConfig.getYaml().setComment("verification.checks.max-login-packets",
"Maximum number of login packets the player has to send in order to be kicked");
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets", 256), 128, 8192);

generalConfig.getYaml().setComment("verification.gamemode",
"The gamemode of the player during verification (0, 1, 2, or 3)");
verification.gamemodeId = (short) clamp(generalConfig.getInt("verification.gamemode", 3), 0, 3);
"The gamemode of the player during verification"
+ LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR"
+ LINE_SEPARATOR + "- SURVIVAL: all UI components are visible"
+ LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden"
+ LINE_SEPARATOR + "- ADVENTURE: all UI components are visible"
+ LINE_SEPARATOR + "- SPECTATOR: all UI components are hidden (Recommended)");
verification.gamemode = Verification.Gamemode.valueOf(
generalConfig.getString("verification.gamemode", Verification.Gamemode.SPECTATOR.name()).toUpperCase());

generalConfig.getYaml().setComment("verification.log-connections",
"Should Sonar log new verification attempts?");
Expand All @@ -436,37 +477,10 @@ public void load() {
+ LINE_SEPARATOR + "This is not recommended for production servers but can be helpful for spotting errors.");
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions", false);

generalConfig.getYaml().setComment("verification.valid-name-regex",
"Regex for validating usernames during verification");
verification.validNameRegex = Pattern.compile(generalConfig.getString(
"verification.valid-name-regex", "^[a-zA-Z0-9_.*!]+$"));

generalConfig.getYaml().setComment("verification.valid-brand-regex",
"Regex for validating client brands during verification");
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
"verification.valid-brand-regex", "^[!-~ ]+$"));

generalConfig.getYaml().setComment("verification.valid-locale-regex",
"Regex for validating client locale during verification");
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
"verification.valid-locale-regex", "^[a-zA-Z_]+$"));

generalConfig.getYaml().setComment("verification.max-brand-length",
"Maximum client brand length during verification");
verification.maxBrandLength = generalConfig.getInt("verification.max-brand-length", 64);

generalConfig.getYaml().setComment("verification.max-ping",
"Ping (in milliseconds) a player has to have in order to timeout");
verification.maxPing = clamp(generalConfig.getInt("verification.max-ping", 10000), 500, 30000);

generalConfig.getYaml().setComment("verification.read-timeout",
"Amount of time that has to pass before a player times out");
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout", 3500), 500, 30000);

generalConfig.getYaml().setComment("verification.max-login-packets",
"Maximum number of login packets the player has to send in order to be kicked");
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.max-login-packets", 256), 128, 8192);

generalConfig.getYaml().setComment("verification.max-players",
"Maximum number of players verifying at the same time");
verification.maxVerifyingPlayers = clamp(generalConfig.getInt("verification.max-players", 1024), 1,
Expand Down Expand Up @@ -1014,9 +1028,9 @@ public void load() {
" <green><bold>%animation%<reset>"
))));
messagesConfig.getYaml().setComment("verbose.animation", "Animation for the action bar"
+ LINE_SEPARATOR + "Alternatives:"
+ LINE_SEPARATOR + "- ▙, ▛, ▜, ▟"
+ LINE_SEPARATOR + "- ⬈, ⬊, ⬋, ⬉");
+ LINE_SEPARATOR + "Alternatives:"
+ LINE_SEPARATOR + "- ▙, ▛, ▜, ▟"
+ LINE_SEPARATOR + "- ⬈, ⬊, ⬋, ⬉");
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation",
Arrays.asList("◜", "◝", "◞", "◟")
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ public static ProtocolVersion fromId(final int protocol) {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}

public boolean inBetween(final ProtocolVersion first, final ProtocolVersion last) {
return compareTo(first) >= 0 && compareTo(last) <= 0;
}

public boolean isUnknown() {
return this == UNKNOWN;
}
Expand Down
3 changes: 2 additions & 1 deletion sonar-bungee/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))

compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")

// MiniMessage platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
Expand Down Expand Up @@ -76,7 +77,7 @@ public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerIn
@Getter
private ChannelWrapper channelWrapper;
private @NotNull final BungeeCord bungee;
private @Nullable FallbackUserWrapper player;
private @Nullable FallbackUserWrapper user;
@Getter
private ProtocolVersion protocolVersion;
private boolean receivedLoginPacket;
Expand Down Expand Up @@ -116,7 +117,7 @@ public void handle(final LoginRequest loginRequest) throws Exception {
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());

// Fix login packet spam exploit
if (receivedLoginPacket || player != null) {
if (receivedLoginPacket || user != null) {
throw new ConditionFailedException("Duplicate login packet");
}
receivedLoginPacket = true;
Expand Down Expand Up @@ -162,14 +163,14 @@ public void handle(final LoginRequest loginRequest) throws Exception {
}

// Create wrapped Fallback user
player = new FallbackUserWrapper(
user = new FallbackUserWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress, protocolVersion
);

// Perform default BungeeCord checks
if (bungee.config.isEnforceSecureProfile()
&& player.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
if (publicKey == null) {
disconnect(bungee.getTranslation("secure_profile_required"));
Expand All @@ -179,7 +180,7 @@ public void handle(final LoginRequest loginRequest) throws Exception {
disconnect(bungee.getTranslation("secure_profile_expired"));
return;
}
if (player.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
&& !EncryptionUtil.check(publicKey, null)) {
disconnect(bungee.getTranslation("secure_profile_invalid"));
return;
Expand Down Expand Up @@ -240,7 +241,7 @@ public void handle(final LoginRequest loginRequest) throws Exception {
);

// Disconnect if the protocol version could not be resolved
if (player.getProtocolVersion().isUnknown()) {
if (user.getProtocolVersion().isUnknown()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
return;
}
Expand All @@ -266,39 +267,39 @@ public void handle(final LoginRequest loginRequest) throws Exception {
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", loginRequest.getData())
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(player.getProtocolVersion().getProtocol())));
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
}
}

// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), player));
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 (player.getPipeline().get(PACKET_ENCODER) == null
|| player.getPipeline().get(PACKET_DECODER) == null) {
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(player.getProtocolVersion());
player.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
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
player.write(new LoginSuccess(loginRequest.getData(), uuid));
user.write(new LoginSuccess(loginRequest.getData(), uuid));

// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);

// Replace normal decoder to allow custom packets
player.getPipeline().replace(
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(player,
new FallbackVerificationHandler(player, loginRequest.getData(), uuid)
user.getPipeline().replace(
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
new FallbackVerificationHandler(user, loginRequest.getData(), uuid)
));
}));
} catch (Throwable throwable) {
Expand All @@ -309,11 +310,11 @@ PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(player,

private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(16);

private static @NotNull Kick getKickPacket(final @NotNull Component 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(serialized);
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
}
return cachedKickPacket;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.protocol.packet.Kick;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bungee.SonarBungee;
Expand Down Expand Up @@ -57,8 +55,7 @@ public void handle(final @NotNull LoginEvent event) {
if (onlinePerIp >= maxOnlinePerIp) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
}
}
}
Expand All @@ -73,8 +70,7 @@ public void handle(final @NotNull PostLoginEvent event) {
if (pendingConnection instanceof FallbackInitialHandler) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
} else {
// Fallback by disconnecting without a message
pendingConnection.disconnect();
Expand Down
Loading

0 comments on commit 0efc6ae

Please sign in to comment.