mirror of
https://github.com/PaperMC/Paper.git
synced 2025-07-26 09:42:06 -07:00
This implements a solution that preemptively exits the tick loop if we have already marked the connection as disconnecting. This avoids changing the result of Connection#isConnected in order to avoid other possibly unintentional changes. Fundamentally it should be investigated if closing the connection async is really still needed. This also additionally removes the login disconnecting logic for server stopping, as this was also a possible issue in the config stage but also shouldn't be an issue as connections are closed on server stop very early. Additionally, do not check for isConnecting() on VERIFYING, as that seemed to be an old diff originally trying to resolve this code, however isConnected is not updated at this point so it pretty much was useless.
308 lines
18 KiB
Diff
308 lines
18 KiB
Diff
--- a/net/minecraft/network/Connection.java
|
|
+++ b/net/minecraft/network/Connection.java
|
|
@@ -74,13 +_,13 @@
|
|
public static final Marker PACKET_RECEIVED_MARKER = Util.make(MarkerFactory.getMarker("PACKET_RECEIVED"), marker -> marker.add(PACKET_MARKER));
|
|
public static final Marker PACKET_SENT_MARKER = Util.make(MarkerFactory.getMarker("PACKET_SENT"), marker -> marker.add(PACKET_MARKER));
|
|
public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(
|
|
- () -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Client IO #%d").setDaemon(true).build())
|
|
+ () -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
|
|
);
|
|
public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(
|
|
- () -> new EpollEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build())
|
|
+ () -> new EpollEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
|
|
);
|
|
public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(
|
|
- () -> new DefaultEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Local Client IO #%d").setDaemon(true).build())
|
|
+ () -> new DefaultEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()) // Paper
|
|
);
|
|
private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
|
|
private final PacketFlow receiving;
|
|
@@ -88,6 +_,11 @@
|
|
private final Queue<Consumer<Connection>> pendingActions = Queues.newConcurrentLinkedQueue();
|
|
public Channel channel;
|
|
public SocketAddress address;
|
|
+ // Spigot start
|
|
+ public java.util.UUID spoofedUUID;
|
|
+ public com.mojang.authlib.properties.Property[] spoofedProfile;
|
|
+ public boolean preparing = true;
|
|
+ // Spigot end
|
|
@Nullable
|
|
private volatile PacketListener disconnectListener;
|
|
@Nullable
|
|
@@ -106,6 +_,41 @@
|
|
private volatile DisconnectionDetails delayedDisconnect;
|
|
@Nullable
|
|
BandwidthDebugMonitor bandwidthDebugMonitor;
|
|
+ public String hostname = ""; // CraftBukkit - add field
|
|
+ // Paper start - NetworkClient implementation
|
|
+ public int protocolVersion;
|
|
+ public java.net.InetSocketAddress virtualHost;
|
|
+ private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing
|
|
+ // Paper end
|
|
+ // Paper start - add utility methods
|
|
+ public final net.minecraft.server.level.ServerPlayer getPlayer() {
|
|
+ if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) {
|
|
+ return impl.player;
|
|
+ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
|
|
+ return null;
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+ // Paper end - add utility methods
|
|
+ // Paper start - packet limiter
|
|
+ protected final Object PACKET_LIMIT_LOCK = new Object();
|
|
+ protected final @Nullable io.papermc.paper.util.IntervalledCounter allPacketCounts = io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.isEnabled() ? new io.papermc.paper.util.IntervalledCounter(
|
|
+ (long)(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.interval() * 1.0e9)
|
|
+ ) : null;
|
|
+ protected final java.util.Map<Class<? extends net.minecraft.network.protocol.Packet<?>>, io.papermc.paper.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>();
|
|
+
|
|
+ private boolean stopReadingPackets;
|
|
+ private void killForPacketSpam() {
|
|
+ this.sendPacket(new ClientboundDisconnectPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage)), PacketSendListener.thenRun(() -> {
|
|
+ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage));
|
|
+ }), true);
|
|
+ this.setReadOnly();
|
|
+ this.stopReadingPackets = true;
|
|
+ }
|
|
+ // Paper end - packet limiter
|
|
+ @Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
|
|
+ public @Nullable java.util.Optional<net.minecraft.network.chat.Component> legacySavedLoginEventResultOverride; // Paper - playerloginevent
|
|
+ public @Nullable net.minecraft.server.level.ServerPlayer savedPlayerForLoginEventLegacy; // Paper - playerloginevent
|
|
|
|
public Connection(PacketFlow receiving) {
|
|
this.receiving = receiving;
|
|
@@ -116,6 +_,7 @@
|
|
super.channelActive(context);
|
|
this.channel = context.channel();
|
|
this.address = this.channel.remoteAddress();
|
|
+ this.preparing = false; // Spigot
|
|
if (this.delayedDisconnect != null) {
|
|
this.disconnect(this.delayedDisconnect);
|
|
}
|
|
@@ -128,14 +_,31 @@
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext context, Throwable exception) {
|
|
+ // Paper start - Handle large packets disconnecting client
|
|
+ if (exception instanceof io.netty.handler.codec.EncoderException && exception.getCause() instanceof PacketEncoder.PacketTooLargeException packetTooLargeException) {
|
|
+ final Packet<?> packet = packetTooLargeException.getPacket();
|
|
+ if (packet.packetTooLarge(this)) {
|
|
+ ProtocolSwapHandler.handleOutboundTerminalPacket(context, packet);
|
|
+ return;
|
|
+ } else if (packet.isSkippable()) {
|
|
+ Connection.LOGGER.debug("Skipping packet due to errors", exception.getCause());
|
|
+ ProtocolSwapHandler.handleOutboundTerminalPacket(context, packet);
|
|
+ return;
|
|
+ } else {
|
|
+ exception = exception.getCause();
|
|
+ }
|
|
+ }
|
|
+ // Paper end - Handle large packets disconnecting client
|
|
if (exception instanceof SkipPacketException) {
|
|
LOGGER.debug("Skipping packet due to errors", exception.getCause());
|
|
} else {
|
|
boolean flag = !this.handlingFault;
|
|
this.handlingFault = true;
|
|
if (this.channel.isOpen()) {
|
|
+ net.minecraft.server.level.ServerPlayer player = this.getPlayer(); // Paper - Add API for quit reason
|
|
if (exception instanceof TimeoutException) {
|
|
LOGGER.debug("Timeout", exception);
|
|
+ if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.TIMED_OUT; // Paper - Add API for quit reason
|
|
this.disconnect(Component.translatable("disconnect.timeout"));
|
|
} else {
|
|
Component component = Component.translatable("disconnect.genericReason", "Internal Exception: " + exception);
|
|
@@ -147,9 +_,11 @@
|
|
disconnectionDetails = new DisconnectionDetails(component);
|
|
}
|
|
|
|
+ if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.ERRONEOUS_STATE; // Paper - Add API for quit reason
|
|
if (flag) {
|
|
LOGGER.debug("Failed to sent packet", exception);
|
|
- if (this.getSending() == PacketFlow.CLIENTBOUND) {
|
|
+ boolean doesDisconnectExist = this.packetListener.protocol() != ConnectionProtocol.STATUS && this.packetListener.protocol() != ConnectionProtocol.HANDSHAKING; // Paper
|
|
+ if (this.getSending() == PacketFlow.CLIENTBOUND && doesDisconnectExist) { // Paper
|
|
Packet<?> packet = (Packet<?>)(this.sendLoginDisconnect
|
|
? new ClientboundLoginDisconnectPacket(component)
|
|
: new ClientboundDisconnectPacket(component));
|
|
@@ -166,6 +_,7 @@
|
|
}
|
|
}
|
|
}
|
|
+ if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(exception); // Spigot // Paper
|
|
}
|
|
|
|
@Override
|
|
@@ -175,10 +_,60 @@
|
|
if (packetListener == null) {
|
|
throw new IllegalStateException("Received a packet before the packet listener was initialized");
|
|
} else {
|
|
+ // Paper start - packet limiter
|
|
+ if (this.stopReadingPackets) {
|
|
+ return;
|
|
+ }
|
|
+ if (this.allPacketCounts != null ||
|
|
+ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass())) {
|
|
+ long time = System.nanoTime();
|
|
+ synchronized (PACKET_LIMIT_LOCK) {
|
|
+ if (this.allPacketCounts != null) {
|
|
+ this.allPacketCounts.updateAndAdd(1, time);
|
|
+ if (this.allPacketCounts.getRate() >= io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.maxPacketRate()) {
|
|
+ this.killForPacketSpam();
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (Class<?> check = packet.getClass(); check != Object.class; check = check.getSuperclass()) {
|
|
+ io.papermc.paper.configuration.GlobalConfiguration.PacketLimiter.PacketLimit packetSpecificLimit =
|
|
+ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.get(check);
|
|
+ if (packetSpecificLimit == null || !packetSpecificLimit.isEnabled()) {
|
|
+ continue;
|
|
+ }
|
|
+ io.papermc.paper.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> {
|
|
+ return new io.papermc.paper.util.IntervalledCounter((long)(packetSpecificLimit.interval() * 1.0e9));
|
|
+ });
|
|
+ counter.updateAndAdd(1, time);
|
|
+ if (counter.getRate() >= packetSpecificLimit.maxPacketRate()) {
|
|
+ switch (packetSpecificLimit.action()) {
|
|
+ case DROP:
|
|
+ return;
|
|
+ case KICK:
|
|
+ String deobfedPacketName = io.papermc.paper.util.ObfHelper.INSTANCE.deobfClassName(check.getName());
|
|
+
|
|
+ String playerName;
|
|
+ if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
|
|
+ playerName = impl.getOwner().getName();
|
|
+ } else {
|
|
+ playerName = this.getLoggableAddress(net.minecraft.server.MinecraftServer.getServer().logIPs());
|
|
+ }
|
|
+
|
|
+ Connection.LOGGER.warn("{} kicked for packet spamming: {}", playerName, deobfedPacketName.substring(deobfedPacketName.lastIndexOf(".") + 1));
|
|
+ this.killForPacketSpam();
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Paper end - packet limiter
|
|
if (packetListener.shouldHandleMessage(packet)) {
|
|
try {
|
|
genericsFtw(packet, packetListener);
|
|
} catch (RunningOnDifferentThreadException var5) {
|
|
+ } catch (io.papermc.paper.util.ServerStopRejectedExecutionException ignored) { // Paper - do not prematurely disconnect players on stop
|
|
} catch (RejectedExecutionException var6) {
|
|
this.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown"));
|
|
} catch (ClassCastException var7) {
|
|
@@ -373,10 +_,30 @@
|
|
}
|
|
}
|
|
|
|
+ private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
|
|
+ private static int joinAttemptsThisTick; // Paper - Buffer joins to world
|
|
+ private static int currTick; // Paper - Buffer joins to world
|
|
public void tick() {
|
|
this.flushQueue();
|
|
+ // Paper start - Buffer joins to world
|
|
+ if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) {
|
|
+ Connection.currTick = net.minecraft.server.MinecraftServer.currentTick;
|
|
+ Connection.joinAttemptsThisTick = 0;
|
|
+ }
|
|
+ // Paper end - Buffer joins to world
|
|
if (this.packetListener instanceof TickablePacketListener tickablePacketListener) {
|
|
+ // Paper start - Buffer joins to world
|
|
+ if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener)
|
|
+ || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING
|
|
+ || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) {
|
|
+ // Paper start - detailed watchdog information
|
|
+ net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
|
|
+ try {
|
|
tickablePacketListener.tick();
|
|
+ } finally {
|
|
+ net.minecraft.network.protocol.PacketUtils.packetProcessing.pop();
|
|
+ } // Paper end - detailed watchdog information
|
|
+ } // Paper end - Buffer joins to world
|
|
}
|
|
|
|
if (!this.isConnected() && !this.disconnectionHandled) {
|
|
@@ -384,7 +_,7 @@
|
|
}
|
|
|
|
if (this.channel != null) {
|
|
- this.channel.flush();
|
|
+ if (enableExplicitFlush) this.channel.eventLoop().execute(() -> this.channel.flush()); // Paper - Disable explicit network manager flushing; we don't need to explicit flush here, but allow opt in incase issues are found to a better version
|
|
}
|
|
|
|
if (this.tickCount++ % 20 == 0) {
|
|
@@ -420,12 +_,13 @@
|
|
}
|
|
|
|
public void disconnect(DisconnectionDetails disconnectionDetails) {
|
|
+ this.preparing = false; // Spigot
|
|
if (this.channel == null) {
|
|
this.delayedDisconnect = disconnectionDetails;
|
|
}
|
|
|
|
if (this.isConnected()) {
|
|
- this.channel.close().awaitUninterruptibly();
|
|
+ this.channel.close(); // We can't wait as this may be called from an event loop.
|
|
this.disconnectionDetails = disconnectionDetails;
|
|
}
|
|
}
|
|
@@ -572,6 +_,13 @@
|
|
}
|
|
}
|
|
|
|
+ // Paper start - add proper async disconnect
|
|
+ public void enableAutoRead() {
|
|
+ if (this.channel != null) {
|
|
+ this.channel.config().setAutoRead(true);
|
|
+ }
|
|
+ }
|
|
+ // Paper end - add proper async disconnect
|
|
public void setupCompression(int threshold, boolean validateDecompressed) {
|
|
if (threshold >= 0) {
|
|
if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder compressionDecoder) {
|
|
@@ -585,6 +_,7 @@
|
|
} else {
|
|
this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(threshold));
|
|
}
|
|
+ this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners
|
|
} else {
|
|
if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder) {
|
|
this.channel.pipeline().remove("decompress");
|
|
@@ -593,6 +_,7 @@
|
|
if (this.channel.pipeline().get("compress") instanceof CompressionEncoder) {
|
|
this.channel.pipeline().remove("compress");
|
|
}
|
|
+ this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_DISABLED); // Paper - Add Channel initialization listeners
|
|
}
|
|
}
|
|
|
|
@@ -610,6 +_,26 @@
|
|
);
|
|
packetListener1.onDisconnect(disconnectionDetails);
|
|
}
|
|
+ this.pendingActions.clear(); // Free up packet queue.
|
|
+ // Paper start - Add PlayerConnectionCloseEvent
|
|
+ if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
|
|
+ /* Player was logged in, either game listener or configuration listener */
|
|
+ final com.mojang.authlib.GameProfile profile = commonPacketListener.getOwner();
|
|
+ new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(),
|
|
+ profile.getName(), ((InetSocketAddress) this.address).getAddress(), false).callEvent();
|
|
+ } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginListener) {
|
|
+ /* Player is login stage */
|
|
+ switch (loginListener.state) {
|
|
+ case VERIFYING:
|
|
+ case WAITING_FOR_DUPE_DISCONNECT:
|
|
+ case PROTOCOL_SWITCHING:
|
|
+ case ACCEPTED:
|
|
+ final com.mojang.authlib.GameProfile profile = loginListener.authenticatedProfile; /* Should be non-null at this stage */
|
|
+ new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(), profile.getName(),
|
|
+ ((InetSocketAddress) this.address).getAddress(), false).callEvent();
|
|
+ }
|
|
+ }
|
|
+ // Paper end - Add PlayerConnectionCloseEvent
|
|
}
|
|
}
|
|
}
|