From 4d608f059c9821cec51bae8daa3cda6f2918cd56 Mon Sep 17 00:00:00 2001 From: luk Date: Mon, 9 Feb 2026 18:39:45 +0000 Subject: [PATCH] dev --- .../core/io/transport/QUICTransport.java | 244 ++++++++++++++++++ .../core/io/transport/TCPTransport.java | 70 +++++ 2 files changed, 314 insertions(+) create mode 100644 src/com/hypixel/hytale/server/core/io/transport/QUICTransport.java create mode 100644 src/com/hypixel/hytale/server/core/io/transport/TCPTransport.java diff --git a/src/com/hypixel/hytale/server/core/io/transport/QUICTransport.java b/src/com/hypixel/hytale/server/core/io/transport/QUICTransport.java new file mode 100644 index 00000000..8d6b6b68 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/io/transport/QUICTransport.java @@ -0,0 +1,244 @@ +package com.hypixel.hytale.server.core.io.transport; + +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.protocol.io.netty.ProtocolUtil; +import com.hypixel.hytale.protocol.packets.connection.Disconnect; +import com.hypixel.hytale.protocol.packets.connection.DisconnectType; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.auth.CertificateUtil; +import com.hypixel.hytale.server.core.auth.ServerAuthManager; +import com.hypixel.hytale.server.core.io.netty.HytaleChannelInitializer; +import com.hypixel.hytale.server.core.io.netty.NettyUtil; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.SocketProtocolFamily; +import io.netty.channel.socket.nio.NioChannelOption; +import io.netty.handler.codec.quic.InsecureQuicTokenHandler; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicCongestionControlAlgorithm; +import io.netty.handler.codec.quic.QuicServerCodecBuilder; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.AttributeKey; +import jdk.net.ExtendedSocketOptions; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.net.ssl.SSLEngine; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +public class QUICTransport implements Transport { + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + public static final AttributeKey CLIENT_CERTIFICATE_ATTR = AttributeKey.valueOf("CLIENT_CERTIFICATE"); + public static final AttributeKey ALPN_REJECT_ERROR_CODE_ATTR = AttributeKey.valueOf("ALPN_REJECT_ERROR_CODE"); + @Nonnull + private final EventLoopGroup workerGroup = NettyUtil.getEventLoopGroup("ServerWorkerGroup"); + private final Bootstrap bootstrapIpv4; + private final Bootstrap bootstrapIpv6; + + public QUICTransport() throws InterruptedException { + SelfSignedCertificate ssc = null; + + try { + ssc = new SelfSignedCertificate("localhost"); + } catch (CertificateException var5) { + throw new RuntimeException(var5); + } + + ServerAuthManager.getInstance().setServerCertificate(ssc.cert()); + LOGGER.at(Level.INFO).log("Server certificate registered for mutual auth, fingerprint: %s", CertificateUtil.computeCertificateFingerprint(ssc.cert())); + QuicSslContext sslContext = QuicSslContextBuilder.forServer(ssc.key(), null, ssc.cert()) + .applicationProtocols("hytale/2", "hytale/1") + .earlyData(false) + .clientAuth(ClientAuth.REQUIRE) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + NettyUtil.ReflectiveChannelFactory channelFactoryIpv4 = NettyUtil.getDatagramChannelFactory(SocketProtocolFamily.INET); + LOGGER.at(Level.INFO).log("Using IPv4 Datagram Channel: %s...", channelFactoryIpv4.getSimpleName()); + this.bootstrapIpv4 = new Bootstrap() + .group(this.workerGroup) + .channelFactory(channelFactoryIpv4) + .option(ChannelOption.SO_REUSEADDR, true) + .option(NioChannelOption.of(ExtendedSocketOptions.IP_DONTFRAGMENT), true) + .handler(new QUICTransport.QuicChannelInboundHandlerAdapter(sslContext)) + .validate(); + NettyUtil.ReflectiveChannelFactory channelFactoryIpv6 = NettyUtil.getDatagramChannelFactory(SocketProtocolFamily.INET6); + LOGGER.at(Level.INFO).log("Using IPv6 Datagram Channel: %s...", channelFactoryIpv6.getSimpleName()); + this.bootstrapIpv6 = new Bootstrap() + .group(this.workerGroup) + .channelFactory(channelFactoryIpv6) + .option(ChannelOption.SO_REUSEADDR, true) + .option(NioChannelOption.of(ExtendedSocketOptions.IP_DONTFRAGMENT), true) + .handler(new QUICTransport.QuicChannelInboundHandlerAdapter(sslContext)) + .validate(); + // Do not call Bootstrap.register() without retaining/closing the created channel. + // bind(...) will register as needed. + } + + @Nonnull + @Override + public TransportType getType() { + return TransportType.QUIC; + } + + @Override + public ChannelFuture bind(@Nonnull InetSocketAddress address) throws InterruptedException { + if (address.getAddress() instanceof Inet4Address) { + return this.bootstrapIpv4.bind(address).sync(); + } else if (address.getAddress() instanceof Inet6Address) { + return this.bootstrapIpv6.bind(address).sync(); + } else { + throw new UnsupportedOperationException("Unsupported address type: " + address.getAddress().getClass()); + } + } + + @Override + public void shutdown() { + LOGGER.at(Level.INFO).log("Shutting down workerGroup..."); + + try { + this.workerGroup.shutdownGracefully(0L, 1L, TimeUnit.SECONDS).await(1L, TimeUnit.SECONDS); + } catch (InterruptedException var2) { + LOGGER.at(Level.SEVERE).withCause(var2).log("Failed to await for listener to close!"); + Thread.currentThread().interrupt(); + } + } + + private static class QuicChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter { + private final QuicSslContext sslContext; + + public QuicChannelInboundHandlerAdapter(QuicSslContext sslContext) { + this.sslContext = sslContext; + } + + @Override + public boolean isSharable() { + return true; + } + + @Override + public void channelActive(@Nonnull ChannelHandlerContext ctx) throws Exception { + Duration playTimeout = HytaleServer.get().getConfig().getConnectionTimeouts().getPlay(); + ChannelHandler quicHandler = new QuicServerCodecBuilder() + .sslContext(this.sslContext) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + .maxIdleTimeout(playTimeout.toMillis(), TimeUnit.MILLISECONDS) + .ackDelayExponent(3L) + .initialMaxData(524288L) + .initialMaxStreamDataUnidirectional(0L) + .initialMaxStreamsUnidirectional(0L) + .initialMaxStreamDataBidirectionalLocal(131072L) + .initialMaxStreamDataBidirectionalRemote(131072L) + .initialMaxStreamsBidirectional(1L) + .discoverPmtu(true) + .congestionControlAlgorithm(QuicCongestionControlAlgorithm.BBR) + .handler( + new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void channelActive(@Nonnull ChannelHandlerContext ctx) throws Exception { + QuicChannel channel = (QuicChannel) ctx.channel(); + QUICTransport.LOGGER + .at(Level.INFO) + .log("Received connection from %s to %s", NettyUtil.formatRemoteAddress(channel), NettyUtil.formatLocalAddress(channel)); + String negotiatedAlpn = channel.sslEngine().getApplicationProtocol(); + int negotiatedVersion = this.parseProtocolVersion(negotiatedAlpn); + if (negotiatedVersion < 2) { + QUICTransport.LOGGER + .at(Level.INFO) + .log("Marking connection from %s for rejection: ALPN %s < required %d", NettyUtil.formatRemoteAddress(channel), negotiatedAlpn, 2); + channel.attr(QUICTransport.ALPN_REJECT_ERROR_CODE_ATTR).set(5); + } + + X509Certificate clientCert = QuicChannelInboundHandlerAdapter.this.extractClientCertificate(channel); + if (clientCert == null) { + QUICTransport.LOGGER + .at(Level.WARNING) + .log("Connection rejected: no client certificate from %s", NettyUtil.formatRemoteAddress(channel)); + ProtocolUtil.closeConnection(channel); + } else { + channel.attr(QUICTransport.CLIENT_CERTIFICATE_ATTR).set(clientCert); + QUICTransport.LOGGER.at(Level.FINE).log("Client certificate: %s", clientCert.getSubjectX500Principal().getName()); + } + } + + private int parseProtocolVersion(String alpn) { + if (alpn != null && alpn.startsWith("hytale/")) { + try { + return Integer.parseInt(alpn.substring(7)); + } catch (NumberFormatException var3) { + return 0; + } + } else { + return 0; + } + } + + @Override + public void channelInactive(@Nonnull ChannelHandlerContext ctx) { + ((QuicChannel) ctx.channel()).collectStats().addListener(f -> { + if (f.isSuccess()) { + QUICTransport.LOGGER.at(Level.INFO).log("Connection closed: %s", f.getNow()); + } + }); + } + + @Override + public void exceptionCaught(@Nonnull ChannelHandlerContext ctx, Throwable cause) { + QUICTransport.LOGGER.at(Level.WARNING).withCause(cause).log("Got exception from netty pipeline in ChannelInitializer!"); + Channel channel = ctx.channel(); + if (channel.isWritable()) { + channel.writeAndFlush(new Disconnect("Internal server error!", DisconnectType.Crash)).addListener(ProtocolUtil.CLOSE_ON_COMPLETE); + } else { + ProtocolUtil.closeApplicationConnection(channel); + } + } + } + ) + .streamHandler(new HytaleChannelInitializer()) + .build(); + ctx.channel().pipeline().addLast(quicHandler); + } + + @Nullable + private X509Certificate extractClientCertificate(QuicChannel channel) { + try { + SSLEngine sslEngine = channel.sslEngine(); + if (sslEngine == null) { + return null; + } + + Certificate[] peerCerts = sslEngine.getSession().getPeerCertificates(); + if (peerCerts != null && peerCerts.length > 0 && peerCerts[0] instanceof X509Certificate) { + return (X509Certificate) peerCerts[0]; + } + } catch (Exception var4) { + QUICTransport.LOGGER.at(Level.FINEST).log("No peer certificate available: %s", var4.getMessage()); + } + + return null; + } + } +} diff --git a/src/com/hypixel/hytale/server/core/io/transport/TCPTransport.java b/src/com/hypixel/hytale/server/core/io/transport/TCPTransport.java new file mode 100644 index 00000000..2603047d --- /dev/null +++ b/src/com/hypixel/hytale/server/core/io/transport/TCPTransport.java @@ -0,0 +1,70 @@ +package com.hypixel.hytale.server.core.io.transport; + +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.io.netty.HytaleChannelInitializer; +import com.hypixel.hytale.server.core.io.netty.NettyUtil; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; + +import javax.annotation.Nonnull; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +public class TCPTransport implements Transport { + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + @Nonnull + private final EventLoopGroup bossGroup = NettyUtil.getEventLoopGroup(1, "ServerBossGroup"); + @Nonnull + private final EventLoopGroup workerGroup = NettyUtil.getEventLoopGroup("ServerWorkerGroup"); + private final ServerBootstrap bootstrap; + + public TCPTransport() throws InterruptedException { + Class serverChannel = NettyUtil.getServerChannel(); + LOGGER.at(Level.INFO).log("Using Server Channel: %s...", serverChannel.getSimpleName()); + this.bootstrap = new ServerBootstrap() + .group(this.bossGroup, this.workerGroup) + .channel(serverChannel) + .option(ChannelOption.SO_BACKLOG, 256) + .option(ChannelOption.SO_REUSEADDR, true) + .childHandler(new HytaleChannelInitializer()) + .validate(); + // Avoid pre-registering an unbound server channel. + // On Epoll this can cause a tight busy-loop on EPOLLHUP (Netty #16240). + } + + @Nonnull + @Override + public TransportType getType() { + return TransportType.TCP; + } + + @Override + public ChannelFuture bind(InetSocketAddress address) throws InterruptedException { + return this.bootstrap.bind(address).sync(); + } + + @Override + public void shutdown() { + LOGGER.at(Level.INFO).log("Shutting down bossGroup..."); + + try { + this.bossGroup.shutdownGracefully(0L, 1L, TimeUnit.SECONDS).await(1L, TimeUnit.SECONDS); + } catch (InterruptedException var3) { + LOGGER.at(Level.SEVERE).withCause(var3).log("Failed to await for listener to close!"); + Thread.currentThread().interrupt(); + } + + LOGGER.at(Level.INFO).log("Shutting down workerGroup..."); + + try { + this.workerGroup.shutdownGracefully(0L, 1L, TimeUnit.SECONDS).await(1L, TimeUnit.SECONDS); + } catch (InterruptedException var2) { + LOGGER.at(Level.SEVERE).withCause(var2).log("Failed to await for listener to close!"); + Thread.currentThread().interrupt(); + } + } +}