Skip to content

Commit

Permalink
Use Netty directly instead of going through the system TCP stack
Browse files Browse the repository at this point in the history
  • Loading branch information
Gaming32 committed May 12, 2024
1 parent c099986 commit a0e4689
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 87 deletions.
78 changes: 0 additions & 78 deletions src/main/java/io/github/gaming32/worldhost/ProxyClient.java

This file was deleted.

13 changes: 6 additions & 7 deletions src/main/java/io/github/gaming32/worldhost/WorldHost.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.github.gaming32.worldhost.protocol.ProtocolClient;
import io.github.gaming32.worldhost.protocol.proxy.ProxyPassthrough;
import io.github.gaming32.worldhost.protocol.proxy.ProxyProtocolClient;
import io.github.gaming32.worldhost.proxy.ProxyClient;
import io.github.gaming32.worldhost.toast.WHToast;
import io.github.gaming32.worldhost.upnp.Gateway;
import io.github.gaming32.worldhost.upnp.GatewayFinder;
Expand Down Expand Up @@ -53,6 +54,7 @@
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -223,6 +225,8 @@ public class WorldHost

public static boolean shareWorldOnLoad;

public static SocketAddress proxySocketAddress;

//#if FABRIC
@Override
public void onInitializeClient() {
Expand Down Expand Up @@ -726,23 +730,18 @@ public static void proxyConnect(long connectionId, InetAddress remoteAddr, Suppl
return;
}
try {
final ProxyClient proxyClient = new ProxyClient(server.getPort(), remoteAddr, connectionId, proxy);
final ProxyClient proxyClient = new ProxyClient(remoteAddr, connectionId, proxy);
WorldHost.CONNECTED_PROXY_CLIENTS.put(connectionId, proxyClient);
proxyClient.start();
} catch (IOException e) {
WorldHost.LOGGER.error("Failed to start ProxyClient", e);
}
}

// TODO: Implement using a proper Netty channel to introduce packets directly to the Netty pipeline somehow.
public static void proxyPacket(long connectionId, byte[] data) {
final ProxyClient proxyClient = WorldHost.CONNECTED_PROXY_CLIENTS.get(connectionId);
if (proxyClient != null) {
try {
proxyClient.getOutputStream().write(data);
} catch (IOException e) {
WorldHost.LOGGER.error("Failed to write to ProxyClient", e);
}
proxyClient.send(data);
} else {
WorldHost.LOGGER.warn("Received packet for unknown connection {}", connectionId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package io.github.gaming32.worldhost.mixin;

import com.mojang.datafixers.DataFixer;
import io.github.gaming32.worldhost.ProxyClient;
import io.github.gaming32.worldhost.proxy.ProxyChannels;
import io.github.gaming32.worldhost.proxy.ProxyClient;
import io.github.gaming32.worldhost.WorldHost;
import io.github.gaming32.worldhost.versions.Components;
import net.minecraft.ChatFormatting;
Expand Down Expand Up @@ -148,4 +149,16 @@ private void shareWorldOnLoad(UUID uuid, CallbackInfo ci) {
Components.copyOnClickText(externalIp), port
);
}

@Inject(
method = "publishServer",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/network/ServerConnectionListener;startTcpServerListener(Ljava/net/InetAddress;I)V",
shift = At.Shift.AFTER
)
)
private void startProxyChannel(GameType gameMode, boolean cheats, int port, CallbackInfoReturnable<Boolean> cir) {
WorldHost.proxySocketAddress = ProxyChannels.startProxyChannel(getConnection());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.gaming32.worldhost.mixin;

import io.netty.channel.ChannelFuture;
import net.minecraft.server.network.ServerConnectionListener;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;

import java.util.List;

@Mixin(ServerConnectionListener.class)
public interface ServerConnectionListenerAccessor {
@Accessor
List<ChannelFuture> getChannels();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.github.gaming32.worldhost.proxy;

import io.github.gaming32.worldhost.mixin.ServerConnectionListenerAccessor;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalServerChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;
import net.minecraft.network.Connection;
import net.minecraft.network.RateKickingConnection;
import net.minecraft.network.protocol.PacketFlow;
import net.minecraft.server.network.LegacyQueryHandler;
import net.minecraft.server.network.ServerConnectionListener;
import net.minecraft.server.network.ServerHandshakePacketListenerImpl;

import java.net.SocketAddress;

public class ProxyChannels {
public static SocketAddress startProxyChannel(ServerConnectionListener listener) {
final ServerConnectionListenerAccessor accessor = (ServerConnectionListenerAccessor)listener;
ChannelFuture channel;
synchronized (accessor.getChannels()) {
channel = new ServerBootstrap()
.channel(LocalServerChannel.class)
.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
final ChannelPipeline pipeline = ch.pipeline().addLast("timeout", new ReadTimeoutHandler(30));
if (listener.getServer().repliesToStatus()) {
pipeline.addLast("legacy_query", new LegacyQueryHandler(listener.getServer()));
}
Connection.configureSerialization(pipeline, PacketFlow.SERVERBOUND, false, null);
final int rateLimit = listener.getServer().getRateLimitPacketsPerSecond();
final Connection connection = rateLimit > 0
? new RateKickingConnection(rateLimit)
: new Connection(PacketFlow.SERVERBOUND);
listener.getConnections().add(connection);
connection.configurePacketHandler(pipeline);
connection.setListenerForServerboundHandshake(
new ServerHandshakePacketListenerImpl(listener.getServer(), connection)
);
}
})
.group(ServerConnectionListener.SERVER_EVENT_GROUP.get())
.localAddress(LocalAddress.ANY)
.bind()
.syncUninterruptibly();
accessor.getChannels().add(channel);
}
return channel.channel().localAddress();
}
}
122 changes: 122 additions & 0 deletions src/main/java/io/github/gaming32/worldhost/proxy/ProxyClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.github.gaming32.worldhost.proxy;

import io.github.gaming32.worldhost.WorldHost;
import io.github.gaming32.worldhost.protocol.proxy.ProxyPassthrough;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.local.LocalChannel;
import net.minecraft.server.network.ServerConnectionListener;

import java.io.IOException;
import java.net.InetAddress;
import java.util.function.Supplier;

public final class ProxyClient extends SimpleChannelInboundHandler<ByteBuf> {
private static final int PACKET_SIZE = 0xffff;

private final InetAddress remoteAddress;
private final long connectionId;
private final Supplier<ProxyPassthrough> proxy;

private Channel channel;
private boolean closed;

public ProxyClient(
InetAddress remoteAddress,
long connectionId,
Supplier<ProxyPassthrough> proxy
) throws IOException {
this.remoteAddress = remoteAddress;
this.connectionId = connectionId;
this.proxy = proxy;
if (proxy.get() == null) {
WorldHost.LOGGER.error("ProxyPassthrough for {} ({}) is initially null.", connectionId, remoteAddress);
}
}

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
channel = ctx.channel();
WorldHost.LOGGER.info("Started proxy client from {}", remoteAddress);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
WorldHost.CONNECTED_PROXY_CLIENTS.remove(connectionId);
close();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
WorldHost.LOGGER.error("Proxy client connection for {} had error", remoteAddress, cause);
WorldHost.CONNECTED_PROXY_CLIENTS.remove(connectionId);
close();
}

@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
final ProxyPassthrough proxy = this.proxy.get();
if (proxy == null) {
close();
return;
}

while (true) {
int len = Math.min(msg.readableBytes(), PACKET_SIZE);
if (len == 0) break;
final byte[] buffer = new byte[len];
msg.readBytes(buffer);
proxy.proxyS2CPacket(connectionId, buffer);
}
}

public void start() {
WorldHost.LOGGER.info("Starting proxy client from {}", remoteAddress);
new Bootstrap()
.group(ServerConnectionListener.SERVER_EVENT_GROUP.get())
.handler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast("handler", ProxyClient.this);
}
})
.channel(LocalChannel.class)
.connect(WorldHost.proxySocketAddress)
.syncUninterruptibly();
}

public void close() {
if (closed) return;
closed = true;
try {
channel.close();
final ProxyPassthrough proxy = this.proxy.get();
if (proxy != null) {
proxy.proxyDisconnect(connectionId);
}
WorldHost.LOGGER.info("Proxy client connection for {} closed", remoteAddress);
} catch (Exception e) {
WorldHost.LOGGER.error("Proxy client connection for {} failed to close", remoteAddress, e);
}
}

public void send(byte[] message) {
if (channel.eventLoop().inEventLoop()) {
doSend(message);
} else {
channel.eventLoop().execute(() -> doSend(message));
}
}

private void doSend(byte[] message) {
channel.writeAndFlush(Unpooled.wrappedBuffer(message))
.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
}
}
3 changes: 2 additions & 1 deletion src/main/resources/world-host.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"mixins": [
"MixinCommands",
"MixinLevelSummary",
"MixinPublishCommand"
"MixinPublishCommand",
"ServerConnectionListenerAccessor"
],
"client": [
"MinecraftAccessor",
Expand Down

0 comments on commit a0e4689

Please sign in to comment.