--- 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 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 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 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 INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; private final PacketFlow receiving; @@ -88,6 +_,11 @@ private final Queue> 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 +_,40 @@ 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) { + org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer(); + return player == null ? null : player.getHandle(); + } + 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>, 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 Connection(PacketFlow receiving) { this.receiving = receiving; @@ -116,6 +_,9 @@ super.channelActive(context); this.channel = context.channel(); this.address = this.channel.remoteAddress(); + // Spigot Start + this.preparing = false; + // Spigot End 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) { @@ -385,10 +_,24 @@ } } + 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) { tickablePacketListener.tick(); + } // Paper end - Buffer joins to world } if (!this.isConnected() && !this.disconnectionHandled) { @@ -396,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) { @@ -432,12 +_,15 @@ } public void disconnect(DisconnectionDetails disconnectionDetails) { + // Spigot Start + this.preparing = false; + // Spigot End 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; } } @@ -584,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) { @@ -597,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"); @@ -605,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 } } @@ -622,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 } } }