Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ You can join the [Discord](https://discord.gg/5z4GuSnqmQ) for help with this for
- New incoming connection batches additional packets to more closely imitate the vanilla client:
- A `Connected Ping`
- The first game packet, `Request Network Settings Packet`
- Attempts to detect it from the connection, but uses `RakChannelOption.RAK_CLIENT_BEDROCK_PROTOCOL_VERSION` if it is not detected in the pipeline
- Allows for resetting security state if `Open Connection Reply 1` is resent by the server
- Only do retries with `Open Connection Request 1`, and reserve `Open Connection Request 2` only as a direct response to `Open Connection Reply 1`
- Allows using datagram channel factories for raknet (from [@AlexProgrammerDE](https://github.com/AlexProgrammerDE))
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Only update version on publishing to Maven Central
version=1.1.0
version=1.2.0
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public class RakChannelOption<T> extends ChannelOption<T> {
valueOf(RakChannelOption.class, "RAK_TIME_BETWEEN_SEND_CONNECTION_ATTEMPTS_MS");

/**
* The protocol version of the RakNet client for sending RequestNetworkSettingsPacket in compatibility mode.
* The fllback protocol version of the RakNet client for sending RequestNetworkSettingsPacket in compatibility mode if one is not found in the pipeline.
*/
public static final ChannelOption<Integer> RAK_CLIENT_BEDROCK_PROTOCOL_VERSION =
valueOf(RakChannelOption.class, "RAK_CLIENT_BEDROCK_PROTOCOL_VERSION");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.cloudburstmc.netty.handler.codec.raknet.client;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.util.AttributeKey;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import org.cloudburstmc.netty.channel.raknet.RakChannel;
import org.cloudburstmc.netty.channel.raknet.config.RakChannelOption;

import static org.cloudburstmc.netty.channel.raknet.RakConstants.ID_GAME_PACKET;

public class RakClientNetworkSettingsHandler extends ChannelOutboundHandlerAdapter {
public static final String NAME = "rak-client-network-settings-handler";
public static final AttributeKey<ByteBuf> NETWORK_SETTINGS_PAYLOAD = AttributeKey.valueOf("network-settings-payload");
private static final InternalLogger log = InternalLoggerFactory.getInstance(RakClientNetworkSettingsHandler.class);

private final RakChannel channel;

public RakClientNetworkSettingsHandler(RakChannel channel) {
this.channel = channel;
}

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof ByteBuf)) {
ctx.write(msg, promise);
return;
}

ByteBuf packet = (ByteBuf) msg;

if (packet.capacity() < 4) {
ctx.write(msg, promise);
return;
}

if (packet.getByte(0) != (byte) ID_GAME_PACKET) {
ctx.write(msg, promise);
return;
}

int rakVersion = this.channel.config().getOption(RakChannelOption.RAK_PROTOCOL_VERSION);

switch (rakVersion) {
case 11:
case 10:
case 9:
if (packet.getByte(1) != (byte) 0x06) break;
if (packet.getByte(2) != (byte) 0xc1) break;
if ((packet.getByte(3) & (byte) 0b10000111) != (byte) 0b00000001) break;
onNetworkSettings(ctx, packet);
return;
case 8:
if (packet.getByte(1) != (byte) 0x07) break;
if (packet.getByte(2) != 0xc1) break;
onNetworkSettings(ctx, packet);
return;
case 7:
if (packet.getByte(1) != (byte) 0x05) break;
if (packet.getByte(2) != (byte) 0xc1) break;
onNetworkSettings(ctx, packet);
return;
default:
throw new UnsupportedOperationException("Unsupported protocol version: " + rakVersion);
}

ctx.write(msg, promise);
}

private void onNetworkSettings(ChannelHandlerContext ctx, ByteBuf packet) {
log.info("Detected network settings packet, removing handler");
ctx.channel().attr(NETWORK_SETTINGS_PAYLOAD).set(packet.retain());
ctx.channel().pipeline().remove(RakClientNetworkSettingsHandler.NAME);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ public RakClientOfflineHandler(RakChannel rakChannel, ChannelPromise promise) {
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
long timeout = this.rakChannel.config().getOption(RakChannelOption.RAK_CONNECT_TIMEOUT);
long timeout = this.rakChannel().config().getOption(RakChannelOption.RAK_CONNECT_TIMEOUT);
this.timeoutFuture = channel.eventLoop().schedule(this::onTimeout, timeout, TimeUnit.MILLISECONDS);
this.retryFuture = channel.eventLoop().scheduleAtFixedRate(() -> this.onRetryAttempt(channel), 0,
this.rakChannel.config().getOption(RakChannelOption.RAK_TIME_BETWEEN_SEND_CONNECTION_ATTEMPTS_MS), TimeUnit.MILLISECONDS);
this.successPromise.addListener(future -> safeCancel(this.timeoutFuture, channel));
this.successPromise.addListener(future -> safeCancel(this.retryFuture, channel));
this.rakChannel().config().getOption(RakChannelOption.RAK_TIME_BETWEEN_SEND_CONNECTION_ATTEMPTS_MS), TimeUnit.MILLISECONDS);
this.successPromise().addListener(future -> safeCancel(this.timeoutFuture, channel));
this.successPromise().addListener(future -> safeCancel(this.retryFuture, channel));

this.retryFuture.addListener(future -> {
if (future.cause() != null) {
this.successPromise.tryFailure(future.cause());
this.successPromise().tryFailure(future.cause());
}
});
}
Expand All @@ -75,32 +75,25 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
safeCancel(this.retryFuture, ctx.channel());
}

private void onRetryAttempt(Channel channel) {
if (this.rakChannel.config().getOption(RakChannelOption.RAK_COMPATIBILITY_MODE)) {
if (this.state != RakOfflineState.HANDSHAKE_COMPLETED) {
void onRetryAttempt(Channel channel) {
switch (this.state()) {
case HANDSHAKE_1:
this.sendOpenConnectionRequest1(channel);
this.connectionAttempts++;
}
} else {
switch (this.state) {
case HANDSHAKE_1:
this.sendOpenConnectionRequest1(channel);
this.connectionAttempts++;
break;
case HANDSHAKE_2:
this.sendOpenConnectionRequest2(channel);
break;
}
this.incrementConnectionAttempts();
break;
case HANDSHAKE_2:
this.sendOpenConnectionRequest2(channel);
break;
}
}

private void onTimeout() {
this.successPromise.tryFailure(new ConnectTimeoutException());
void onTimeout() {
this.successPromise().tryFailure(new ConnectTimeoutException());
}

private void onSuccess(ChannelHandlerContext ctx) {
void onSuccess(ChannelHandlerContext ctx) {
// Create new session which decodes RakDatagramPacket to RakMessage
RakSessionCodec sessionCodec = new RakSessionCodec(this.rakChannel);
RakSessionCodec sessionCodec = new RakSessionCodec(this.rakChannel());
ctx.pipeline().addAfter(NAME, RakDatagramCodec.NAME, new RakDatagramCodec());
ctx.pipeline().addAfter(RakDatagramCodec.NAME, RakAcknowledgeHandler.NAME, new RakAcknowledgeHandler(sessionCodec));
ctx.pipeline().addAfter(RakAcknowledgeHandler.NAME, RakSessionCodec.NAME, sessionCodec);
Expand All @@ -109,8 +102,7 @@ private void onSuccess(ChannelHandlerContext ctx) {
ctx.pipeline().addAfter(ConnectedPongHandler.NAME, DisconnectNotificationHandler.NAME, DisconnectNotificationHandler.INSTANCE);
// Replicate server behavior, and transform unhandled encapsulated packets to rakMessage
ctx.pipeline().addAfter(DisconnectNotificationHandler.NAME, EncapsulatedToMessageHandler.NAME, EncapsulatedToMessageHandler.INSTANCE);
ctx.pipeline().addAfter(DisconnectNotificationHandler.NAME, RakClientOnlineInitialHandler.NAME, new RakClientOnlineInitialHandler(this.rakChannel, this.successPromise));
ctx.pipeline().fireChannelActive();
ctx.pipeline().addAfter(DisconnectNotificationHandler.NAME, RakClientOnlineInitialHandler.NAME, new RakClientOnlineInitialHandler(this.rakChannel(), this.successPromise()));
}

@Override
Expand All @@ -119,16 +111,16 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Excep
return; // Empty packet?
}

if (this.state == RakOfflineState.HANDSHAKE_COMPLETED) {
if (this.state() == RakOfflineState.HANDSHAKE_COMPLETED) {
// Forward open connection messages if handshake was completed
ctx.fireChannelRead(buf.retain());
return;
}

short packetId = buf.readUnsignedByte();
ByteBuf magicBuf = this.rakChannel.config().getOption(RakChannelOption.RAK_UNCONNECTED_MAGIC);
ByteBuf magicBuf = this.rakChannel().config().getOption(RakChannelOption.RAK_UNCONNECTED_MAGIC);
if (!buf.isReadable(magicBuf.readableBytes()) || !ByteBufUtil.equals(buf.readSlice(magicBuf.readableBytes()), magicBuf)) {
this.successPromise.tryFailure(new CorruptedFrameException("RakMagic does not match"));
this.successPromise().tryFailure(new CorruptedFrameException("RakMagic does not match"));
return;
}

Expand All @@ -139,27 +131,28 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Excep
case ID_OPEN_CONNECTION_REPLY_2:
this.onOpenConnectionReply2(ctx, buf);
this.onSuccess(ctx);
ctx.pipeline().fireChannelActive();
return;
case ID_INCOMPATIBLE_PROTOCOL_VERSION:
this.rakChannel.pipeline().fireUserEventTriggered(RakDisconnectReason.INCOMPATIBLE_PROTOCOL_VERSION);
this.successPromise.tryFailure(new IllegalStateException("Incompatible raknet version"));
this.rakChannel().pipeline().fireUserEventTriggered(RakDisconnectReason.INCOMPATIBLE_PROTOCOL_VERSION);
this.successPromise().tryFailure(new IllegalStateException("Incompatible raknet version"));
return;
case ID_ALREADY_CONNECTED:
this.rakChannel.pipeline().fireUserEventTriggered(RakDisconnectReason.ALREADY_CONNECTED);
this.successPromise.tryFailure(new ChannelException("Already connected"));
this.rakChannel().pipeline().fireUserEventTriggered(RakDisconnectReason.ALREADY_CONNECTED);
this.successPromise().tryFailure(new ChannelException("Already connected"));
return;
case ID_NO_FREE_INCOMING_CONNECTIONS:
this.rakChannel.pipeline().fireUserEventTriggered(RakDisconnectReason.NO_FREE_INCOMING_CONNECTIONS);
this.successPromise.tryFailure(new ChannelException("No free incoming connections"));
this.rakChannel().pipeline().fireUserEventTriggered(RakDisconnectReason.NO_FREE_INCOMING_CONNECTIONS);
this.successPromise().tryFailure(new ChannelException("No free incoming connections"));
return;
case ID_IP_RECENTLY_CONNECTED:
this.rakChannel.pipeline().fireUserEventTriggered(RakDisconnectReason.IP_RECENTLY_CONNECTED);
this.successPromise.tryFailure(new ChannelException("Address recently connected"));
this.rakChannel().pipeline().fireUserEventTriggered(RakDisconnectReason.IP_RECENTLY_CONNECTED);
this.successPromise().tryFailure(new ChannelException("Address recently connected"));
return;
}
}

private void onOpenConnectionReply1(ChannelHandlerContext ctx, ByteBuf buffer) {
void onOpenConnectionReply1(ChannelHandlerContext ctx, ByteBuf buffer) {
long serverGuid = buffer.readLong();
boolean security = buffer.readBoolean();
if (security) {
Expand All @@ -170,38 +163,35 @@ private void onOpenConnectionReply1(ChannelHandlerContext ctx, ByteBuf buffer) {
}
int mtu = buffer.readShort();

this.rakChannel.config().setOption(RakChannelOption.RAK_MTU, mtu);
this.rakChannel.config().setOption(RakChannelOption.RAK_REMOTE_GUID, serverGuid);
this.rakChannel().config().setOption(RakChannelOption.RAK_MTU, mtu);
this.rakChannel().config().setOption(RakChannelOption.RAK_REMOTE_GUID, serverGuid);

this.state = RakOfflineState.HANDSHAKE_2;
this.state(RakOfflineState.HANDSHAKE_2);
this.sendOpenConnectionRequest2(ctx.channel());
}

private void onOpenConnectionReply2(ChannelHandlerContext ctx, ByteBuf buffer) {
void onOpenConnectionReply2(ChannelHandlerContext ctx, ByteBuf buffer) {
buffer.readLong(); // serverGuid
if (this.rakChannel.config().getOption(RakChannelOption.RAK_COMPATIBILITY_MODE)) {
RakUtils.skipAddress(buffer); // serverAddress
} else {
RakUtils.readAddress(buffer); // serverAddress
}
RakUtils.readAddress(buffer); // serverAddress

int mtu = buffer.readShort();
boolean security = buffer.readBoolean(); // security
if (security) {
this.successPromise.tryFailure(new SecurityException());
this.successPromise().tryFailure(new SecurityException());
return;
}

this.rakChannel.config().setOption(RakChannelOption.RAK_MTU, mtu);
this.state = RakOfflineState.HANDSHAKE_COMPLETED;
this.rakChannel().config().setOption(RakChannelOption.RAK_MTU, mtu);
this.state(RakOfflineState.HANDSHAKE_COMPLETED);
}

private void sendOpenConnectionRequest1(Channel channel) {
int mtuSizeIndex = Math.min(this.connectionAttempts / 4, this.rakChannel.config().getOption(RakChannelOption.RAK_MTU_SIZES).length - 1);
int mtuSize = this.rakChannel.config().getOption(RakChannelOption.RAK_MTU_SIZES)[mtuSizeIndex];
void sendOpenConnectionRequest1(Channel channel) {
int mtuSizeIndex = Math.min(this.connectionAttempts() / 4, this.rakChannel().config().getOption(RakChannelOption.RAK_MTU_SIZES).length - 1);
int mtuSize = this.rakChannel().config().getOption(RakChannelOption.RAK_MTU_SIZES)[mtuSizeIndex];

ByteBuf magicBuf = this.rakChannel.config().getOption(RakChannelOption.RAK_UNCONNECTED_MAGIC);
int rakVersion = this.rakChannel.config().getOption(RakChannelOption.RAK_PROTOCOL_VERSION);
InetSocketAddress address = (InetSocketAddress) this.rakChannel.remoteAddress();
ByteBuf magicBuf = this.rakChannel().config().getOption(RakChannelOption.RAK_UNCONNECTED_MAGIC);
int rakVersion = this.rakChannel().config().getOption(RakChannelOption.RAK_PROTOCOL_VERSION);
InetSocketAddress address = (InetSocketAddress) this.rakChannel().remoteAddress();

ByteBuf request = channel.alloc().ioBuffer(mtuSize);
request.writeByte(ID_OPEN_CONNECTION_REQUEST_1);
Expand All @@ -212,9 +202,9 @@ private void sendOpenConnectionRequest1(Channel channel) {
channel.writeAndFlush(request);
}

private void sendOpenConnectionRequest2(Channel channel) {
int mtuSize = this.rakChannel.config().getOption(RakChannelOption.RAK_MTU);
ByteBuf magicBuf = this.rakChannel.config().getOption(RakChannelOption.RAK_UNCONNECTED_MAGIC);
void sendOpenConnectionRequest2(Channel channel) {
int mtuSize = this.rakChannel().config().getOption(RakChannelOption.RAK_MTU);
ByteBuf magicBuf = this.rakChannel().config().getOption(RakChannelOption.RAK_UNCONNECTED_MAGIC);

ByteBuf request = channel.alloc().ioBuffer(this.security ? 39 : 34);
request.writeByte(ID_OPEN_CONNECTION_REQUEST_2);
Expand All @@ -225,11 +215,35 @@ private void sendOpenConnectionRequest2(Channel channel) {
}
RakUtils.writeAddress(request, (InetSocketAddress) channel.remoteAddress());
request.writeShort(mtuSize);
request.writeLong(this.rakChannel.config().getOption(RakChannelOption.RAK_GUID));
request.writeLong(this.rakChannel().config().getOption(RakChannelOption.RAK_GUID));
channel.writeAndFlush(request);
}

private static void safeCancel(ScheduledFuture<?> future, Channel channel) {
RakOfflineState state() {
return this.state;
}

void state(RakOfflineState state) {
this.state = state;
}

int connectionAttempts() {
return this.connectionAttempts;
}

void incrementConnectionAttempts() {
this.connectionAttempts++;
}

RakChannel rakChannel() {
return this.rakChannel;
}

ChannelPromise successPromise() {
return this.successPromise;
}

static void safeCancel(ScheduledFuture<?> future, Channel channel) {
channel.eventLoop().execute(() -> { // Make sure this is not called at two places at the same time
if (!future.isCancelled()) {
future.cancel(false);
Expand Down
Loading