Skip to content

Commit 730acaa

Browse files
authored
Support cookies (#38)
* Support cookies Signed-off-by: Joshua Castle <[email protected]> * Address existing GH reviews - Add RAK_SEND_COOKIE to DefaultRakServerConfig#getOptions() - Consolidate RakClientOfflineHandler#sendOpenConnectionRequest2 to single method - Combine pendingConnections ExpiringMap Signed-off-by: GitHub <[email protected]> * Remove unused packages Signed-off-by: Joshua Castle <[email protected]> * Create provider for preferred SecureRandom algorithm Signed-off-by: Joshua Castle <[email protected]> * preferredAlgorithms in block * Why we should not do web edits Signed-off-by: Joshua Castle <[email protected]> --------- Signed-off-by: Joshua Castle <[email protected]> Signed-off-by: GitHub <[email protected]>
1 parent 04d4c83 commit 730acaa

File tree

10 files changed

+192
-41
lines changed

10 files changed

+192
-41
lines changed

transport-raknet/src/main/java/org/cloudburstmc/netty/channel/raknet/RakChannel.java

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.cloudburstmc.netty.channel.raknet;
1818

1919
import io.netty.channel.Channel;
20-
import io.netty.channel.ChannelConfig;
2120
import io.netty.channel.ChannelPipeline;
2221
import org.cloudburstmc.netty.channel.raknet.config.RakChannelConfig;
2322

transport-raknet/src/main/java/org/cloudburstmc/netty/channel/raknet/config/DefaultRakServerConfig.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class DefaultRakServerConfig extends DefaultChannelConfig implements RakS
4646
private volatile int packetLimit = RakConstants.DEFAULT_PACKET_LIMIT;
4747
private volatile int globalPacketLimit = RakConstants.DEFAULT_GLOBAL_PACKET_LIMIT;
4848
private volatile RakServerMetrics metrics;
49+
private volatile boolean sendCookie;
4950

5051
public DefaultRakServerConfig(RakServerChannel channel) {
5152
super(channel);
@@ -56,7 +57,8 @@ public Map<ChannelOption<?>, Object> getOptions() {
5657
return getOptions(
5758
super.getOptions(),
5859
RakChannelOption.RAK_GUID, RakChannelOption.RAK_MAX_CHANNELS, RakChannelOption.RAK_MAX_CONNECTIONS, RakChannelOption.RAK_SUPPORTED_PROTOCOLS, RakChannelOption.RAK_UNCONNECTED_MAGIC,
59-
RakChannelOption.RAK_ADVERTISEMENT, RakChannelOption.RAK_HANDLE_PING, RakChannelOption.RAK_PACKET_LIMIT, RakChannelOption.RAK_GLOBAL_PACKET_LIMIT, RakChannelOption.RAK_SERVER_METRICS);
60+
RakChannelOption.RAK_ADVERTISEMENT, RakChannelOption.RAK_HANDLE_PING, RakChannelOption.RAK_PACKET_LIMIT, RakChannelOption.RAK_GLOBAL_PACKET_LIMIT, RakChannelOption.RAK_SEND_COOKIE,
61+
RakChannelOption.RAK_SERVER_METRICS);
6062
}
6163

6264
@SuppressWarnings("unchecked")
@@ -98,6 +100,9 @@ public <T> T getOption(ChannelOption<T> option) {
98100
if (option == RakChannelOption.RAK_SERVER_METRICS) {
99101
return (T) this.getMetrics();
100102
}
103+
if (option == RakChannelOption.RAK_SEND_COOKIE) {
104+
return (T) Boolean.valueOf(this.sendCookie);
105+
}
101106
return this.channel.parent().config().getOption(option);
102107
}
103108

@@ -127,6 +132,8 @@ public <T> boolean setOption(ChannelOption<T> option, T value) {
127132
this.setPacketLimit((Integer) value);
128133
} else if (option == RakChannelOption.RAK_GLOBAL_PACKET_LIMIT) {
129134
this.setGlobalPacketLimit((Integer) value);
135+
} else if (option == RakChannelOption.RAK_SEND_COOKIE) {
136+
this.sendCookie = (Boolean) value;
130137
} else if (option == RakChannelOption.RAK_SERVER_METRICS) {
131138
this.setMetrics((RakServerMetrics) value);
132139
} else{
@@ -265,6 +272,16 @@ public void setGlobalPacketLimit(int globalPacketLimit) {
265272
this.globalPacketLimit = globalPacketLimit;
266273
}
267274

275+
@Override
276+
public void setSendCookie(boolean sendCookie) {
277+
this.sendCookie = sendCookie;
278+
}
279+
280+
@Override
281+
public boolean getSendCookie() {
282+
return this.sendCookie;
283+
}
284+
268285
@Override
269286
public void setMetrics(RakServerMetrics metrics) {
270287
this.metrics = metrics;

transport-raknet/src/main/java/org/cloudburstmc/netty/channel/raknet/config/RakChannelOption.java

+6
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ public class RakChannelOption<T> extends ChannelOption<T> {
152152
public static final ChannelOption<Integer> RAK_GLOBAL_PACKET_LIMIT =
153153
valueOf(RakChannelOption.class, "RAK_GLOBAL_PACKET_LIMIT");
154154

155+
/**
156+
* Whether to send a cookie to the client during the connection process.
157+
*/
158+
public static final ChannelOption<Boolean> RAK_SEND_COOKIE =
159+
valueOf(RakChannelOption.class, "RAK_SEND_COOKIE");
160+
155161
@SuppressWarnings("deprecation")
156162
protected RakChannelOption() {
157163
super(null);

transport-raknet/src/main/java/org/cloudburstmc/netty/channel/raknet/config/RakServerChannelConfig.java

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ public interface RakServerChannelConfig extends ChannelConfig {
6565

6666
void setGlobalPacketLimit(int limit);
6767

68+
void setSendCookie(boolean sendCookie);
69+
70+
boolean getSendCookie();
71+
6872
void setMetrics(RakServerMetrics metrics);
6973

7074
RakServerMetrics getMetrics();

transport-raknet/src/main/java/org/cloudburstmc/netty/handler/codec/raknet/client/RakClientOfflineHandler.java

+15-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import io.netty.buffer.ByteBuf;
2020
import io.netty.buffer.ByteBufUtil;
2121
import io.netty.channel.*;
22-
import io.netty.channel.socket.DatagramPacket;
2322
import io.netty.handler.codec.CorruptedFrameException;
2423
import io.netty.util.concurrent.ScheduledFuture;
2524
import org.cloudburstmc.netty.channel.raknet.RakChannel;
@@ -45,6 +44,8 @@ public class RakClientOfflineHandler extends SimpleChannelInboundHandler<ByteBuf
4544

4645
private RakOfflineState state = RakOfflineState.HANDSHAKE_1;
4746
private int connectionAttempts;
47+
private int cookie;
48+
private boolean security;
4849

4950
public RakClientOfflineHandler(RakChannel rakChannel, ChannelPromise promise) {
5051
this.rakChannel = rakChannel;
@@ -153,11 +154,11 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Excep
153154
private void onOpenConnectionReply1(ChannelHandlerContext ctx, ByteBuf buffer) {
154155
long serverGuid = buffer.readLong();
155156
boolean security = buffer.readBoolean();
156-
int mtu = buffer.readShort();
157157
if (security) {
158-
this.successPromise.tryFailure(new SecurityException());
159-
return;
158+
this.cookie = buffer.readInt();
159+
this.security = true;
160160
}
161+
int mtu = buffer.readShort();
161162

162163
this.rakChannel.config().setOption(RakChannelOption.RAK_MTU, mtu);
163164
this.rakChannel.config().setOption(RakChannelOption.RAK_REMOTE_GUID, serverGuid);
@@ -170,7 +171,11 @@ private void onOpenConnectionReply2(ChannelHandlerContext ctx, ByteBuf buffer) {
170171
buffer.readLong(); // serverGuid
171172
RakUtils.readAddress(buffer); // serverAddress
172173
int mtu = buffer.readShort();
173-
buffer.readBoolean(); // security
174+
boolean security = buffer.readBoolean(); // security
175+
if (security) {
176+
this.successPromise.tryFailure(new SecurityException());
177+
return;
178+
}
174179

175180
this.rakChannel.config().setOption(RakChannelOption.RAK_MTU, mtu);
176181
this.state = RakOfflineState.HANDSHAKE_COMPLETED;
@@ -200,9 +205,13 @@ private void sendOpenConnectionRequest2(Channel channel) {
200205
int mtuSize = this.rakChannel.config().getOption(RakChannelOption.RAK_MTU);
201206
ByteBuf magicBuf = this.rakChannel.config().getOption(RakChannelOption.RAK_UNCONNECTED_MAGIC);
202207

203-
ByteBuf request = channel.alloc().ioBuffer(34);
208+
ByteBuf request = channel.alloc().ioBuffer(this.security ? 39 : 34);
204209
request.writeByte(ID_OPEN_CONNECTION_REQUEST_2);
205210
request.writeBytes(magicBuf, magicBuf.readerIndex(), magicBuf.readableBytes());
211+
if (this.security) {
212+
request.writeInt(this.cookie);
213+
request.writeBoolean(false); // Client wrote challenge
214+
}
206215
RakUtils.writeAddress(request, (InetSocketAddress) channel.remoteAddress());
207216
request.writeShort(mtuSize);
208217
request.writeLong(this.rakChannel.config().getOption(RakChannelOption.RAK_GUID));

transport-raknet/src/main/java/org/cloudburstmc/netty/handler/codec/raknet/client/RakClientOnlineInitialHandler.java

-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919

2020
import io.netty.buffer.ByteBuf;
21-
import io.netty.buffer.ByteBufUtil;
2221
import io.netty.channel.Channel;
2322
import io.netty.channel.ChannelHandlerContext;
2423
import io.netty.channel.ChannelPromise;
@@ -32,7 +31,6 @@
3231
import org.cloudburstmc.netty.util.RakUtils;
3332

3433
import java.net.InetSocketAddress;
35-
import java.util.concurrent.TimeUnit;
3634

3735
import static org.cloudburstmc.netty.channel.raknet.RakConstants.*;
3836

transport-raknet/src/main/java/org/cloudburstmc/netty/handler/codec/raknet/server/RakServerOfflineHandler.java

+69-13
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,18 @@
2626
import net.jodah.expiringmap.ExpirationPolicy;
2727
import net.jodah.expiringmap.ExpiringMap;
2828
import org.cloudburstmc.netty.channel.raknet.RakChildChannel;
29-
import org.cloudburstmc.netty.channel.raknet.RakConstants;
3029
import org.cloudburstmc.netty.channel.raknet.RakPing;
3130
import org.cloudburstmc.netty.channel.raknet.RakServerChannel;
3231
import org.cloudburstmc.netty.channel.raknet.config.RakChannelOption;
3332
import org.cloudburstmc.netty.channel.raknet.config.RakServerMetrics;
3433
import org.cloudburstmc.netty.handler.codec.raknet.AdvancedChannelInboundHandler;
3534
import org.cloudburstmc.netty.util.RakUtils;
35+
import org.cloudburstmc.netty.util.SecureAlgorithmProvider;
3636

3737
import java.net.Inet6Address;
3838
import java.net.InetSocketAddress;
39+
import java.security.NoSuchAlgorithmException;
40+
import java.security.SecureRandom;
3941
import java.util.Arrays;
4042
import java.util.concurrent.TimeUnit;
4143

@@ -46,7 +48,15 @@ public class RakServerOfflineHandler extends AdvancedChannelInboundHandler<Datag
4648

4749
private static final InternalLogger log = InternalLoggerFactory.getInstance(RakServerOfflineHandler.class);
4850

49-
private final ExpiringMap<InetSocketAddress, Integer> pendingConnections = ExpiringMap.builder()
51+
private final ThreadLocal<SecureRandom> random = ThreadLocal.withInitial(() -> {
52+
try {
53+
return SecureRandom.getInstance(SecureAlgorithmProvider.getSecurityAlgorithm());
54+
} catch (NoSuchAlgorithmException e) {
55+
return new SecureRandom();
56+
}
57+
});
58+
59+
private final ExpiringMap<InetSocketAddress, PendingConnection> pendingConnections = ExpiringMap.builder()
5060
.expiration(10, TimeUnit.SECONDS)
5161
.expirationPolicy(ExpirationPolicy.CREATED)
5262
.expirationListener((key, value) -> ReferenceCountUtil.release(value))
@@ -163,16 +173,31 @@ private void onOpenConnectionRequest1(ChannelHandlerContext ctx, DatagramPacket
163173
// TODO: banned address check?
164174
// TODO: max connections check?
165175

166-
Integer version = this.pendingConnections.put(sender, protocolVersion);
167-
if (version != null && log.isTraceEnabled()) {
176+
177+
boolean sendCookie = ctx.channel().config().getOption(RakChannelOption.RAK_SEND_COOKIE);
178+
int cookie;
179+
180+
if (sendCookie) {
181+
cookie = this.random.get().nextInt();
182+
} else {
183+
cookie = 0;
184+
}
185+
186+
PendingConnection connection = this.pendingConnections.put(sender, new PendingConnection(protocolVersion, cookie));
187+
if (connection != null && log.isTraceEnabled()) {
168188
log.trace("Received duplicate open connection request 1 from {}", sender);
169189
}
170190

171-
ByteBuf replyBuffer = ctx.alloc().ioBuffer(28, 28);
191+
int bufferCapacity = sendCookie ? 32 : 28; // 4 byte cookie
192+
193+
ByteBuf replyBuffer = ctx.alloc().ioBuffer(bufferCapacity, bufferCapacity);
172194
replyBuffer.writeByte(ID_OPEN_CONNECTION_REPLY_1);
173195
replyBuffer.writeBytes(magicBuf, magicBuf.readerIndex(), magicBuf.readableBytes());
174196
replyBuffer.writeLong(guid);
175-
replyBuffer.writeBoolean(false); // Security
197+
replyBuffer.writeBoolean(sendCookie); // Security
198+
if (sendCookie) {
199+
replyBuffer.writeInt(cookie);
200+
}
176201
replyBuffer.writeShort(RakUtils.clamp(mtu, ctx.channel().config().getOption(RakChannelOption.RAK_MIN_MTU), ctx.channel().config().getOption(RakChannelOption.RAK_MAX_MTU)));
177202
ctx.writeAndFlush(new DatagramPacket(replyBuffer, sender));
178203
}
@@ -183,18 +208,31 @@ private void onOpenConnectionRequest2(ChannelHandlerContext ctx, DatagramPacket
183208
// Skip already verified magic
184209
buffer.skipBytes(magicBuf.readableBytes());
185210

186-
Integer version = this.pendingConnections.remove(sender);
187-
if (version == null) {
188-
// We can't determine the version without the previous request, so assume it's the wrong version.
211+
212+
PendingConnection connection = this.pendingConnections.remove(sender);
213+
if (connection == null) {
189214
if (log.isTraceEnabled()) {
190215
log.trace("Received open connection request 2 from {} without open connection request 1", sender);
191216
}
192-
int[] supportedProtocols = ctx.channel().config().getOption(RakChannelOption.RAK_SUPPORTED_PROTOCOLS);
193-
int latestVersion = supportedProtocols == null ? RakConstants.RAKNET_PROTOCOL_VERSION : supportedProtocols[supportedProtocols.length - 1];
194-
this.sendIncompatibleVersion(ctx, sender, latestVersion, magicBuf, guid);
217+
// Don't respond yet as we cannot verify the connection source IP
195218
return;
196219
}
197220

221+
boolean sendCookie = ctx.channel().config().getOption(RakChannelOption.RAK_SEND_COOKIE);
222+
if (sendCookie) {
223+
int cookie = buffer.readInt();
224+
int expectedCookie = connection.getCookie();
225+
if (expectedCookie != cookie) {
226+
if (log.isTraceEnabled()) {
227+
log.trace("Received open connection request 2 from {} with invalid cookie (expected {}, but received {})", sender, expectedCookie, cookie);
228+
}
229+
// Incorrect cookie provided
230+
// This is likely source IP spoofing so we will not reply
231+
return;
232+
}
233+
buffer.readBoolean(); // Client wrote challenge
234+
}
235+
198236
// TODO: Verify serverAddress matches?
199237
InetSocketAddress serverAddress = RakUtils.readAddress(buffer);
200238
int mtu = buffer.readUnsignedShort();
@@ -207,7 +245,7 @@ private void onOpenConnectionRequest2(ChannelHandlerContext ctx, DatagramPacket
207245
}
208246

209247
RakServerChannel serverChannel = (RakServerChannel) ctx.channel();
210-
RakChildChannel channel = serverChannel.createChildChannel(sender, clientGuid, version, mtu);
248+
RakChildChannel channel = serverChannel.createChildChannel(sender, clientGuid, connection.getProtocolVersion(), mtu);
211249
if (channel == null) {
212250
// Already connected
213251
this.sendAlreadyConnected(ctx, sender, magicBuf, guid);
@@ -240,4 +278,22 @@ private void sendAlreadyConnected(ChannelHandlerContext ctx, InetSocketAddress s
240278
buffer.writeLong(guid);
241279
ctx.writeAndFlush(new DatagramPacket(buffer, sender));
242280
}
281+
282+
private class PendingConnection {
283+
private final int protocolVersion;
284+
private final int cookie;
285+
286+
public PendingConnection(int protocolVersion, int cookie) {
287+
this.protocolVersion = protocolVersion;
288+
this.cookie = cookie;
289+
}
290+
291+
public int getProtocolVersion() {
292+
return this.protocolVersion;
293+
}
294+
295+
public int getCookie() {
296+
return this.cookie;
297+
}
298+
}
243299
}

transport-raknet/src/main/java/org/cloudburstmc/netty/util/RakUtils.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public static InetSocketAddress readAddress(ByteBuf buffer) {
8888
int scopeId = buffer.readInt();
8989
address = Inet6Address.getByAddress(null, addressBytes, scopeId);
9090
} else {
91-
throw new UnsupportedOperationException("Unknown Internet Protocol version.");
91+
throw new UnsupportedOperationException("Unknown Internet Protocol version. Expected 4 or 6, got " + type);
9292
}
9393
} catch (UnknownHostException e) {
9494
throw new IllegalArgumentException(e);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.cloudburstmc.netty.util;
2+
3+
import java.security.Provider;
4+
import java.security.SecureRandom;
5+
import java.security.Security;
6+
import java.util.Arrays;
7+
import java.util.List;
8+
import java.util.stream.Stream;
9+
10+
public class SecureAlgorithmProvider {
11+
private static final String SECURITY_ALGORITHM;
12+
13+
static {
14+
// SecureRandom algorithms in order of most preferred to least preferred.
15+
final List<String> preferredAlgorithms = Arrays.asList(
16+
"SHA1PRNG",
17+
"NativePRNGNonBlocking",
18+
"Windows-PRNG",
19+
"NativePRNG",
20+
"PKCS11",
21+
"DRBG",
22+
"NativePRNGBlocking"
23+
);
24+
25+
SECURITY_ALGORITHM = Stream.of(Security.getProviders())
26+
.flatMap(provider -> provider.getServices().stream())
27+
.filter(service -> "SecureRandom".equals(service.getType()))
28+
.map(Provider.Service::getAlgorithm)
29+
.filter(preferredAlgorithms::contains)
30+
.min((s1, s2) -> Integer.compare(preferredAlgorithms.indexOf(s1), preferredAlgorithms.indexOf(s2)))
31+
.orElse(new SecureRandom().getAlgorithm());
32+
}
33+
34+
public static String getSecurityAlgorithm() {
35+
return SECURITY_ALGORITHM;
36+
}
37+
}

0 commit comments

Comments
 (0)