Skip to content

Commit

Permalink
Merge pull request #73 from Gaming32/netty-proxy
Browse files Browse the repository at this point in the history
Use Netty directly instead of going through the system TCP stack
  • Loading branch information
Gaming32 committed May 12, 2024
2 parents c099986 + f2bd9ba commit 668dcb0
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 87 deletions.
78 changes: 0 additions & 78 deletions src/main/java/io/github/gaming32/worldhost/ProxyClient.java

This file was deleted.

27 changes: 20 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,11 +11,14 @@
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;
import io.github.gaming32.worldhost.versions.Components;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap;
Expand All @@ -36,6 +39,7 @@
import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket;
import net.minecraft.network.protocol.status.ServerStatus;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.network.ServerConnectionListener;
import net.minecraft.server.players.GameProfileCache;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpGet;
Expand All @@ -52,7 +56,9 @@
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
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 +229,9 @@ public class WorldHost

public static boolean shareWorldOnLoad;

public static SocketAddress proxySocketAddress;
public static Constructor<? extends ChannelInitializer<Channel>> channelInitializerConstructor;

//#if FABRIC
@Override
public void onInitializeClient() {
Expand Down Expand Up @@ -726,23 +735,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 Expand Up @@ -866,6 +870,15 @@ private static Path getGameDir() {
//#endif
}

public static ChannelInitializer<Channel> createChannelInitializer(ServerConnectionListener listener) {
try {
return channelInitializerConstructor.newInstance(listener);
} catch (ReflectiveOperationException e) {
// TODO: UncheckedReflectiveOperationException when 1.20.4+ becomes the minimum
throw new RuntimeException(e);
}
}

//#if FORGELIKE
//#if MC >= 1.20.5
//$$ @EventBusSubscriber(modid = MOD_ID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
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,20 @@
package io.github.gaming32.worldhost.mixin;

import io.github.gaming32.worldhost.WorldHost;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import net.minecraft.server.network.ServerConnectionListener;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(targets = "net.minecraft.server.network.ServerConnectionListener$1")
public abstract class MixinServerConnectionListener_1 extends ChannelInitializer<Channel> {
@Inject(method = "<init>", at = @At("TAIL"))
private void storeClass(ServerConnectionListener this$0, CallbackInfo ci) throws NoSuchMethodException {
if (WorldHost.channelInitializerConstructor == null) {
WorldHost.channelInitializerConstructor = getClass().getDeclaredConstructor(ServerConnectionListener.class);
}
}
}
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,29 @@
package io.github.gaming32.worldhost.proxy;

import io.github.gaming32.worldhost.WorldHost;
import io.github.gaming32.worldhost.mixin.ServerConnectionListenerAccessor;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalServerChannel;
import net.minecraft.server.network.ServerConnectionListener;

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(WorldHost.createChannelInitializer(listener))
.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);
}
}
4 changes: 3 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,9 @@
"mixins": [
"MixinCommands",
"MixinLevelSummary",
"MixinPublishCommand"
"MixinPublishCommand",
"MixinServerConnectionListener_1",
"ServerConnectionListenerAccessor"
],
"client": [
"MinecraftAccessor",
Expand Down

0 comments on commit 668dcb0

Please sign in to comment.