From a2618233029b0bf381b5f6e937ab91d59951fb7e Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Wed, 8 Aug 2018 04:44:27 -0400 Subject: [PATCH 1/7] Add favicon support --- .gitignore | 3 +- .../velocitypowered/api/server/Favicon.java | 88 +++++++++++++++++++ .../com/velocitypowered/proxy/Velocity.java | 6 ++ .../velocitypowered/proxy/VelocityServer.java | 3 + .../proxy/config/VelocityConfiguration.java | 25 ++++++ .../client/StatusSessionHandler.java | 2 +- .../proxy/data/ServerPing.java | 7 +- .../protocol/util/FaviconSerializer.java | 18 ++++ 8 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/com/velocitypowered/api/server/Favicon.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java diff --git a/.gitignore b/.gitignore index 7ae951cf7..0e54c3ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,5 @@ gradle-app.setting # Other trash logs/ -/velocity.toml \ No newline at end of file +/velocity.toml +server-icon.png \ No newline at end of file diff --git a/api/src/main/java/com/velocitypowered/api/server/Favicon.java b/api/src/main/java/com/velocitypowered/api/server/Favicon.java new file mode 100644 index 000000000..ceeaf517c --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/server/Favicon.java @@ -0,0 +1,88 @@ +package com.velocitypowered.api.server; + +import com.google.common.base.Preconditions; + +import javax.annotation.Nonnull; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Objects; + +/** + * Represents a Minecraft server favicon. A Minecraft server favicon is a 64x64 image that can be displayed to a remote + * client that sends a Server List Ping packet, and is automatically displayed in the Minecraft client. + */ +public final class Favicon { + private final String base64Url; + + /** + * Directly create a favicon using its Base64 URL directly. You are generally better served by the create() series + * of functions. + * @param base64Url the url for use with this favicon + */ + public Favicon(@Nonnull String base64Url) { + this.base64Url = Preconditions.checkNotNull(base64Url, "base64Url"); + } + + /** + * Returns the Base64-encoded URI for this image. + * @return a URL representing this favicon + */ + public String getBase64Url() { + return base64Url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Favicon favicon = (Favicon) o; + return Objects.equals(base64Url, favicon.base64Url); + } + + @Override + public int hashCode() { + return Objects.hash(base64Url); + } + + @Override + public String toString() { + return "Favicon{" + + "base64Url='" + base64Url + '\'' + + '}'; + } + + /** + * Creates a new {@code Favicon} from the specified {@code image}. + * @param image the image to use for the favicon + * @return the created {@link Favicon} instance + */ + public static Favicon create(@Nonnull BufferedImage image) { + Preconditions.checkNotNull(image, "image"); + Preconditions.checkArgument(image.getWidth() == 64 && image.getHeight() == 64, "Image does not have" + + " 64x64 dimensions (found %sx%s)", image.getWidth(), image.getHeight()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + ImageIO.write(image, "PNG", os); + } catch (IOException e) { + throw new AssertionError(e); + } + return new Favicon("data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray())); + } + + /** + * Creates a new {@code Favicon} by reading the image from the specified {@code path}. + * @param path the path to the image to create a favicon for + * @return the created {@link Favicon} instance + */ + public static Favicon create(@Nonnull Path path) throws IOException { + try (InputStream stream = Files.newInputStream(path)) { + return create(ImageIO.read(stream)); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java index 6efd6a6c7..e2ee28950 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java @@ -3,6 +3,12 @@ package com.velocitypowered.proxy; import com.velocitypowered.proxy.console.VelocityConsole; public class Velocity { + static { + // We use BufferedImage for favicons, and on macOS this puts the Java application in the dock. How inconvenient. + // Force AWT to work with its head chopped off. + System.setProperty("java.awt.headless", "true"); + } + public static void main(String... args) { final VelocityServer server = VelocityServer.getServer(); server.start(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 3cff9b6bd..e548d54c3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder; import com.velocitypowered.api.command.CommandInvoker; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.server.Favicon; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.network.ConnectionManager; import com.velocitypowered.proxy.command.ServerCommand; @@ -17,6 +18,7 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.http.NettyHttpClient; import com.velocitypowered.api.server.ServerInfo; import com.velocitypowered.proxy.command.CommandManager; +import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.EncryptionUtils; import com.velocitypowered.proxy.util.ServerMap; @@ -44,6 +46,7 @@ public class VelocityServer implements ProxyServer { private static final VelocityServer INSTANCE = new VelocityServer(); public static final Gson GSON = new GsonBuilder() .registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer()) + .registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer()) .create(); private final ConnectionManager cm = new ConnectionManager(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 10f697903..cd5f5536c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -2,6 +2,7 @@ package com.velocitypowered.proxy.config; import com.google.common.collect.ImmutableMap; import com.moandjiezana.toml.Toml; +import com.velocitypowered.api.server.Favicon; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.api.util.LegacyChatColorUtils; import net.kyori.text.Component; @@ -15,6 +16,7 @@ import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,6 +38,7 @@ public class VelocityConfiguration { private final int queryPort; private Component motdAsComponent; + private Favicon favicon; private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode, IPForwardingMode ipForwardingMode, Map servers, @@ -124,9 +127,22 @@ public class VelocityConfiguration { logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance."); } + loadFavicon(); + return valid; } + private void loadFavicon() { + Path faviconPath = Paths.get("server-icon.png"); + if (Files.exists(faviconPath)) { + try { + this.favicon = Favicon.create(faviconPath); + } catch (Exception e) { + logger.info("Unable to load your server-icon.png, continuing without it.", e); + } + } + } + public InetSocketAddress getBind() { return AddressUtil.parseAddress(bind); } @@ -182,6 +198,14 @@ public class VelocityConfiguration { return compressionLevel; } + public Favicon getFavicon() { + return favicon; + } + + public static Logger getLogger() { + return logger; + } + @Override public String toString() { return "VelocityConfiguration{" + @@ -197,6 +221,7 @@ public class VelocityConfiguration { ", queryEnabled=" + queryEnabled + ", queryPort=" + queryPort + ", motdAsComponent=" + motdAsComponent + + ", favicon=" + favicon + '}'; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index e75eafebd..7d5f886bd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -41,7 +41,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler { new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING), new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()), configuration.getMotdComponent(), - null + configuration.getFavicon() ); StatusResponse response = new StatusResponse(); response.setStatus(VelocityServer.GSON.toJson(ping)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java b/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java index 72494eaf1..a6cb2a746 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java @@ -1,14 +1,15 @@ package com.velocitypowered.proxy.data; +import com.velocitypowered.api.server.Favicon; import net.kyori.text.Component; public class ServerPing { private final Version version; private final Players players; private final Component description; - private final String favicon; + private final Favicon favicon; - public ServerPing(Version version, Players players, Component description, String favicon) { + public ServerPing(Version version, Players players, Component description, Favicon favicon) { this.version = version; this.players = players; this.description = description; @@ -27,7 +28,7 @@ public class ServerPing { return description; } - public String getFavicon() { + public Favicon getFavicon() { return favicon; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java new file mode 100644 index 000000000..7243e8b06 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java @@ -0,0 +1,18 @@ +package com.velocitypowered.proxy.protocol.util; + +import com.google.gson.*; +import com.velocitypowered.api.server.Favicon; + +import java.lang.reflect.Type; + +public class FaviconSerializer implements JsonSerializer, JsonDeserializer { + @Override + public Favicon deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new Favicon(json.getAsString()); + } + + @Override + public JsonElement serialize(Favicon src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getBase64Url()); + } +} From 6bc5413038be94aa0dccb85e0709deff48f0e91c Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Wed, 8 Aug 2018 05:59:26 -0400 Subject: [PATCH 2/7] Exclude all Gradle build folders --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0e54c3ff3..cd03f954a 100644 --- a/.gitignore +++ b/.gitignore @@ -102,7 +102,7 @@ hs_err_pid* ### Gradle ### .gradle -/build/ +build/ # Ignore Gradle GUI config gradle-app.setting From 84717a11da41fb63c67941aa27bec5f8ba1d3493 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Wed, 8 Aug 2018 09:22:47 -0400 Subject: [PATCH 3/7] Update README.md --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 018884003..b7348b485 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,6 @@ page. ## Status -Velocity is far from finished, but most of the essential pieces are in place: -you can switch between two servers running Minecraft 1.8-1.13. More versions -and functionality is planned. - -You should join us on **irc.spi.gt** `#velocity` or send us a pull request. +Velocity is far from finished, but most of the essential pieces you would +expect are in place. Velocity supports Minecraft 1.8-1.13. More functionality +is planned. From 512b1c2403ac2c7da8c0cb453d868e283b7ddc63 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Wed, 8 Aug 2018 09:45:27 -0400 Subject: [PATCH 4/7] Remove erroneously added getter --- .../velocitypowered/proxy/config/VelocityConfiguration.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index cd5f5536c..8f2a5aa6d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -202,10 +202,6 @@ public class VelocityConfiguration { return favicon; } - public static Logger getLogger() { - return logger; - } - @Override public String toString() { return "VelocityConfiguration{" + From db8b7c807c2fc7209d7d5ec3f8002e1edc2f849c Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Wed, 8 Aug 2018 10:10:11 -0400 Subject: [PATCH 5/7] Add kqueue transport support --- proxy/build.gradle | 1 + .../network/ConnectionManager.java | 98 ++++++++++++------- 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/proxy/build.gradle b/proxy/build.gradle index ec0a4f7e9..e977e03fc 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -28,6 +28,7 @@ dependencies { compile "io.netty:netty-handler:${nettyVersion}" compile "io.netty:netty-transport-native-epoll:${nettyVersion}" compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64" + compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64" compile "org.apache.logging.log4j:log4j-api:${log4jVersion}" compile "org.apache.logging.log4j:log4j-core:${log4jVersion}" diff --git a/proxy/src/main/java/com/velocitypowered/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/network/ConnectionManager.java index ab5853bc3..35497a361 100644 --- a/proxy/src/main/java/com/velocitypowered/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/network/ConnectionManager.java @@ -24,6 +24,7 @@ import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.kqueue.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; @@ -37,6 +38,7 @@ import org.apache.logging.log4j.Logger; import java.net.InetSocketAddress; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -53,46 +55,25 @@ import static com.velocitypowered.network.Connections.READ_TIMEOUT; public final class ConnectionManager { private static final Logger logger = LogManager.getLogger(ConnectionManager.class); - private static final String DISABLE_EPOLL_PROPERTY = "velocity.connection.disable-epoll"; - private static final boolean DISABLE_EPOLL = Boolean.getBoolean(DISABLE_EPOLL_PROPERTY); private final Set endpoints = new HashSet<>(); - private final Class serverSocketChannelClass; - private final Class socketChannelClass; - private final Class datagramChannelClass; + private final TransportType transportType; private final EventLoopGroup bossGroup; private final EventLoopGroup workerGroup; public ConnectionManager() { - final boolean epoll = canUseEpoll(); - if (epoll) { - this.serverSocketChannelClass = EpollServerSocketChannel.class; - this.socketChannelClass = EpollSocketChannel.class; - this.datagramChannelClass = EpollDatagramChannel.class; - this.bossGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Boss #%d")); - this.workerGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Worker #%d")); - } else { - this.serverSocketChannelClass = NioServerSocketChannel.class; - this.socketChannelClass = NioSocketChannel.class; - this.datagramChannelClass = NioDatagramChannel.class; - this.bossGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Boss #%d")); - this.workerGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Worker #%d")); - } - this.logChannelInformation(epoll); + this.transportType = TransportType.bestType(); + this.bossGroup = transportType.createEventLoopGroup(true); + this.workerGroup = transportType.createEventLoopGroup(false); + this.logChannelInformation(); } - private void logChannelInformation(final boolean epoll) { - final StringBuilder sb = new StringBuilder(); - sb.append("Using channel type "); - sb.append(epoll ? "epoll": "nio"); - if(DISABLE_EPOLL) { - sb.append(String.format(" - epoll explicitly disabled using -D%s=true", DISABLE_EPOLL_PROPERTY)); - } - logger.info(sb.toString()); // TODO: move to logger + private void logChannelInformation() { + logger.info("Using channel type {}", transportType); } public void bind(final InetSocketAddress address) { final ServerBootstrap bootstrap = new ServerBootstrap() - .channel(this.serverSocketChannelClass) + .channel(this.transportType.serverSocketChannelClass) .group(this.bossGroup, this.workerGroup) .childHandler(new ChannelInitializer() { @Override @@ -129,7 +110,7 @@ public final class ConnectionManager { public void queryBind(final String hostname, final int port) { Bootstrap bootstrap = new Bootstrap() - .channel(datagramChannelClass) + .channel(transportType.datagramChannelClass) .group(this.workerGroup) .handler(new GS4QueryHandler()) .localAddress(hostname, port); @@ -147,7 +128,7 @@ public final class ConnectionManager { public Bootstrap createWorker() { return new Bootstrap() - .channel(this.socketChannelClass) + .channel(this.transportType.socketChannelClass) .group(this.workerGroup); } @@ -162,14 +143,61 @@ public final class ConnectionManager { } } - private static boolean canUseEpoll() { - return Epoll.isAvailable() && !DISABLE_EPOLL; - } - private static ThreadFactory createThreadFactory(final String nameFormat) { return new ThreadFactoryBuilder() .setNameFormat(nameFormat) .setDaemon(true) .build(); } + + private enum TransportType { + NIO(NioServerSocketChannel.class, NioSocketChannel.class, NioDatagramChannel.class) { + @Override + public EventLoopGroup createEventLoopGroup(boolean boss) { + String name = "Netty NIO " + (boss ? "Boss" : "Worker") + " #%d"; + return new NioEventLoopGroup(0, createThreadFactory(name)); + } + }, + EPOLL(EpollServerSocketChannel.class, EpollSocketChannel.class, EpollDatagramChannel.class) { + @Override + public EventLoopGroup createEventLoopGroup(boolean boss) { + String name = "Netty Epoll " + (boss ? "Boss" : "Worker") + " #%d"; + return new EpollEventLoopGroup(0, createThreadFactory(name)); + } + }, + KQUEUE(KQueueServerSocketChannel.class, KQueueSocketChannel.class, KQueueDatagramChannel.class) { + @Override + public EventLoopGroup createEventLoopGroup(boolean boss) { + String name = "Netty Kqueue " + (boss ? "Boss" : "Worker") + " #%d"; + return new KQueueEventLoopGroup(0, createThreadFactory(name)); + } + }; + + private final Class serverSocketChannelClass; + private final Class socketChannelClass; + private final Class datagramChannelClass; + + TransportType(Class serverSocketChannelClass, Class socketChannelClass, Class datagramChannelClass) { + this.serverSocketChannelClass = serverSocketChannelClass; + this.socketChannelClass = socketChannelClass; + this.datagramChannelClass = datagramChannelClass; + } + + @Override + public String toString() { + return name().toLowerCase(Locale.US); + } + + public abstract EventLoopGroup createEventLoopGroup(boolean boss); + + public static TransportType bestType() { + if (Epoll.isAvailable()) { + return EPOLL; + } else if (KQueue.isAvailable()) { + return KQUEUE; + } else { + return NIO; + } + } + } } From 254508a5cf941def583ee8f3989ffefddb5f463b Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Thu, 9 Aug 2018 03:23:27 -0400 Subject: [PATCH 6/7] Add connection attempt rate-limiting. --- .../velocitypowered/proxy/VelocityServer.java | 6 +++ .../client/HandshakeSessionHandler.java | 6 +++ .../client/LoginSessionHandler.java | 5 +++ .../proxy/util/Ratelimiter.java | 41 +++++++++++++++++++ .../proxy/util/RatelimiterTest.java | 30 ++++++++++++++ .../proxy/util/ServerMapTest.java | 35 ++++++++++++++++ 6 files changed, 123 insertions(+) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index e548d54c3..d202b9b13 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -21,6 +21,7 @@ import com.velocitypowered.proxy.command.CommandManager; import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.EncryptionUtils; +import com.velocitypowered.proxy.util.Ratelimiter; import com.velocitypowered.proxy.util.ServerMap; import io.netty.bootstrap.Bootstrap; import net.kyori.text.Component; @@ -71,6 +72,7 @@ public class VelocityServer implements ProxyServer { return true; } }; + private final Ratelimiter ipAttemptLimiter = new Ratelimiter(3000); // TODO: Configurable. private VelocityServer() { commandManager.registerCommand("velocity", new VelocityCommand()); @@ -162,6 +164,10 @@ public class VelocityServer implements ProxyServer { return httpClient; } + public Ratelimiter getIpAttemptLimiter() { + return ipAttemptLimiter; + } + public boolean registerConnection(ConnectedPlayer connection) { String lowerName = connection.getUsername().toLowerCase(Locale.US); if (connectionsByName.putIfAbsent(lowerName, connection) != null) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index 700709867..a603e7c8a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -15,6 +15,7 @@ import net.kyori.text.TextComponent; import net.kyori.text.TranslatableComponent; import net.kyori.text.format.TextColor; +import java.net.InetAddress; import java.net.InetSocketAddress; public class HandshakeSessionHandler implements MinecraftSessionHandler { @@ -50,6 +51,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client"))); return; } else { + InetAddress address = ((InetSocketAddress) connection.getChannel().remoteAddress()).getAddress(); + if (!VelocityServer.getServer().getIpAttemptLimiter().attempt(address)) { + connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later."))); + return; + } connection.setSessionHandler(new LoginSessionHandler(connection)); } break; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index cc28c91fa..d08351d14 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -91,6 +91,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { VelocityServer.getServer().getHttpClient() .get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp))) .thenAcceptAsync(profileResponse -> { + if (inbound.isClosed()) { + // The player disconnected after we authenticated them. + return; + } + try { inbound.enableEncryption(decryptedSharedSecret); } catch (GeneralSecurityException e) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java new file mode 100644 index 000000000..2f49c6e6b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java @@ -0,0 +1,41 @@ +package com.velocitypowered.proxy.util; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Ticker; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import java.net.InetAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class Ratelimiter { + private final Cache expiringCache; + private final long timeoutNanos; + + public Ratelimiter(long timeoutMs) { + this(timeoutMs, Ticker.systemTicker()); + } + + @VisibleForTesting + Ratelimiter(long timeoutMs, Ticker ticker) { + this.timeoutNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMs); + this.expiringCache = CacheBuilder.newBuilder() + .ticker(ticker) + .concurrencyLevel(Runtime.getRuntime().availableProcessors()) + .expireAfterWrite(timeoutMs, TimeUnit.MILLISECONDS) + .build(); + } + + public boolean attempt(InetAddress address) { + long expectedNewValue = System.nanoTime() + timeoutNanos; + long last; + try { + last = expiringCache.get(address, () -> expectedNewValue); + } catch (ExecutionException e) { + // It should be impossible for this to fail. + throw new AssertionError(e); + } + return expectedNewValue == last; + } +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java new file mode 100644 index 000000000..7df52cb96 --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java @@ -0,0 +1,30 @@ +package com.velocitypowered.proxy.util; + +import com.google.common.base.Ticker; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.*; + +class RatelimiterTest { + + @Test + void attempt() { + long base = System.nanoTime(); + AtomicLong extra = new AtomicLong(); + Ticker testTicker = new Ticker() { + @Override + public long read() { + return base + extra.get(); + } + }; + Ratelimiter ratelimiter = new Ratelimiter(1000, testTicker); + assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress())); + assertFalse(ratelimiter.attempt(InetAddress.getLoopbackAddress())); + extra.addAndGet(TimeUnit.SECONDS.toNanos(2)); + assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress())); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java new file mode 100644 index 000000000..30d2c617d --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java @@ -0,0 +1,35 @@ +package com.velocitypowered.proxy.util; + +import com.velocitypowered.api.server.ServerInfo; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class ServerMapTest { + private static final InetSocketAddress TEST_ADDRESS = new InetSocketAddress(InetAddress.getLoopbackAddress(), 25565); + + @Test + void respectsCaseInsensitivity() { + ServerMap map = new ServerMap(); + ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS); + map.register(info); + + assertEquals(Optional.of(info), map.getServer("TestServer")); + assertEquals(Optional.of(info), map.getServer("testserver")); + assertEquals(Optional.of(info), map.getServer("TESTSERVER")); + } + + @Test + void rejectsRepeatedRegisterAttempts() { + ServerMap map = new ServerMap(); + ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS); + map.register(info); + + ServerInfo willReject = new ServerInfo("TESTSERVER", TEST_ADDRESS); + assertThrows(IllegalArgumentException.class, () -> map.register(willReject)); + } +} \ No newline at end of file From 1f0a4a8228d9ba572de9673a67a89c35bdff6ed4 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Thu, 9 Aug 2018 05:24:47 -0400 Subject: [PATCH 7/7] Add support for HMACed player forwarding data. This provides a small degree of security but also makes Velocity "secure by default", especially on shared hosts. --- ...ingMode.java => PlayerInfoForwarding.java} | 2 +- .../proxy/config/VelocityConfiguration.java | 52 +++++++++----- .../backend/LoginSessionHandler.java | 68 +++++++++++++------ .../connection/backend/ServerConnection.java | 6 +- proxy/src/main/resources/velocity.toml | 5 +- 5 files changed, 92 insertions(+), 41 deletions(-) rename proxy/src/main/java/com/velocitypowered/proxy/config/{IPForwardingMode.java => PlayerInfoForwarding.java} (69%) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/IPForwardingMode.java b/proxy/src/main/java/com/velocitypowered/proxy/config/PlayerInfoForwarding.java similarity index 69% rename from proxy/src/main/java/com/velocitypowered/proxy/config/IPForwardingMode.java rename to proxy/src/main/java/com/velocitypowered/proxy/config/PlayerInfoForwarding.java index 87d449c2e..078c239c5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/IPForwardingMode.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/PlayerInfoForwarding.java @@ -1,6 +1,6 @@ package com.velocitypowered.proxy.config; -public enum IPForwardingMode { +public enum PlayerInfoForwarding { NONE, LEGACY, MODERN diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 8f2a5aa6d..13c97eff5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -5,6 +5,7 @@ import com.moandjiezana.toml.Toml; import com.velocitypowered.api.server.Favicon; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.api.util.LegacyChatColorUtils; +import io.netty.buffer.ByteBufUtil; import net.kyori.text.Component; import net.kyori.text.serializer.ComponentSerializers; import org.apache.logging.log4j.LogManager; @@ -28,7 +29,7 @@ public class VelocityConfiguration { private final String motd; private final int showMaxPlayers; private final boolean onlineMode; - private final IPForwardingMode ipForwardingMode; + private final PlayerInfoForwarding playerInfoForwardingMode; private final Map servers; private final List attemptConnectionOrder; private final int compressionThreshold; @@ -40,21 +41,25 @@ public class VelocityConfiguration { private Component motdAsComponent; private Favicon favicon; + private final byte[] forwardingSecret; + private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode, - IPForwardingMode ipForwardingMode, Map servers, + PlayerInfoForwarding playerInfoForwardingMode, Map servers, List attemptConnectionOrder, int compressionThreshold, - int compressionLevel, boolean queryEnabled, int queryPort) { + int compressionLevel, boolean queryEnabled, int queryPort, + byte[] forwardingSecret) { this.bind = bind; this.motd = motd; this.showMaxPlayers = showMaxPlayers; this.onlineMode = onlineMode; - this.ipForwardingMode = ipForwardingMode; + this.playerInfoForwardingMode = playerInfoForwardingMode; this.servers = servers; this.attemptConnectionOrder = attemptConnectionOrder; this.compressionThreshold = compressionThreshold; this.compressionLevel = compressionLevel; this.queryEnabled = queryEnabled; this.queryPort = queryPort; + this.forwardingSecret = forwardingSecret; } public boolean validate() { @@ -76,9 +81,15 @@ public class VelocityConfiguration { logger.info("Proxy is running in offline mode!"); } - switch (ipForwardingMode) { + switch (playerInfoForwardingMode) { case NONE: - logger.info("IP forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs."); + logger.info("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs."); + break; + case MODERN: + if (forwardingSecret.length == 0) { + logger.error("You don't have a forwarding secret set."); + valid = false; + } break; } @@ -178,8 +189,8 @@ public class VelocityConfiguration { return onlineMode; } - public IPForwardingMode getIpForwardingMode() { - return ipForwardingMode; + public PlayerInfoForwarding getPlayerInfoForwardingMode() { + return playerInfoForwardingMode; } public Map getServers() { @@ -202,6 +213,10 @@ public class VelocityConfiguration { return favicon; } + public byte[] getForwardingSecret() { + return forwardingSecret; + } + @Override public String toString() { return "VelocityConfiguration{" + @@ -209,7 +224,7 @@ public class VelocityConfiguration { ", motd='" + motd + '\'' + ", showMaxPlayers=" + showMaxPlayers + ", onlineMode=" + onlineMode + - ", ipForwardingMode=" + ipForwardingMode + + ", playerInfoForwardingMode=" + playerInfoForwardingMode + ", servers=" + servers + ", attemptConnectionOrder=" + attemptConnectionOrder + ", compressionThreshold=" + compressionThreshold + @@ -218,6 +233,7 @@ public class VelocityConfiguration { ", queryPort=" + queryPort + ", motdAsComponent=" + motdAsComponent + ", favicon=" + favicon + + ", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret) + '}'; } @@ -236,18 +252,22 @@ public class VelocityConfiguration { } } + byte[] forwardingSecret = toml.getString("player-info-forwarding-secret", "5up3r53cr3t") + .getBytes(StandardCharsets.UTF_8); + return new VelocityConfiguration( - toml.getString("bind"), - toml.getString("motd"), - toml.getLong("show-max-players").intValue(), - toml.getBoolean("online-mode"), - IPForwardingMode.valueOf(toml.getString("ip-forwarding").toUpperCase()), + toml.getString("bind", "0.0.0.0:25577"), + toml.getString("motd", "&3A Velocity Server"), + toml.getLong("show-max-players", 500L).intValue(), + toml.getBoolean("online-mode", true), + PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding", "MODERN").toUpperCase()), ImmutableMap.copyOf(servers), toml.getTable("servers").getList("try"), toml.getTable("advanced").getLong("compression-threshold", 1024L).intValue(), toml.getTable("advanced").getLong("compression-level", -1L).intValue(), - toml.getTable("query").getBoolean("enabled"), - toml.getTable("query").getLong("port", 25577L).intValue()); + toml.getTable("query").getBoolean("enabled", false), + toml.getTable("query").getLong("port", 25577L).intValue(), + forwardingSecret); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index e4219dd16..cf3e776b5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -2,7 +2,8 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.config.IPForwardingMode; +import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.VelocityConstants; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; @@ -17,6 +18,11 @@ import io.netty.buffer.Unpooled; import io.netty.channel.ChannelPipeline; import net.kyori.text.TextComponent; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -30,7 +36,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { @Override public void activated() { - if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN) { + if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN) { forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> { connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), TextComponent.of("Your server did not send the forwarding request in time. Is it set up correctly?")); @@ -44,12 +50,14 @@ public class LoginSessionHandler implements MinecraftSessionHandler { throw new IllegalStateException("Backend server is online-mode!"); } else if (packet instanceof LoginPluginMessage) { LoginPluginMessage message = (LoginPluginMessage) packet; - if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN && + VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration(); + if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && message.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) { LoginPluginResponse response = new LoginPluginResponse(); response.setSuccess(true); response.setId(message.getId()); - response.setData(createForwardingData(connection.getProxyPlayer().getRemoteAddress().getHostString(), + response.setData(createForwardingData(configuration.getForwardingSecret(), + connection.getProxyPlayer().getRemoteAddress().getHostString(), connection.getProxyPlayer().getProfile())); connection.getMinecraftConnection().write(response); cancelForwardingCheck(); @@ -122,23 +130,43 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } } - private static ByteBuf createForwardingData(String address, GameProfile profile) { - ByteBuf buf = Unpooled.buffer(); - ProtocolUtils.writeString(buf, address); - ProtocolUtils.writeUuid(buf, profile.idAsUuid()); - ProtocolUtils.writeString(buf, profile.getName()); - ProtocolUtils.writeVarInt(buf, profile.getProperties().size()); - for (GameProfile.Property property : profile.getProperties()) { - ProtocolUtils.writeString(buf, property.getName()); - ProtocolUtils.writeString(buf, property.getValue()); - String signature = property.getSignature(); - if (signature != null) { - buf.writeBoolean(true); - ProtocolUtils.writeString(buf, signature); - } else { - buf.writeBoolean(false); + static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) { + ByteBuf dataToForward = Unpooled.buffer(); + ByteBuf finalData = Unpooled.buffer(); + try { + ProtocolUtils.writeString(dataToForward, address); + ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid()); + ProtocolUtils.writeString(dataToForward, profile.getName()); + ProtocolUtils.writeVarInt(dataToForward, profile.getProperties().size()); + for (GameProfile.Property property : profile.getProperties()) { + ProtocolUtils.writeString(dataToForward, property.getName()); + ProtocolUtils.writeString(dataToForward, property.getValue()); + String signature = property.getSignature(); + if (signature != null) { + dataToForward.writeBoolean(true); + ProtocolUtils.writeString(dataToForward, signature); + } else { + dataToForward.writeBoolean(false); + } } + + SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(key); + mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes()); + byte[] sig = mac.doFinal(); + finalData.writeBytes(sig); + finalData.writeBytes(dataToForward); + return finalData; + } catch (InvalidKeyException e) { + finalData.release(); + throw new RuntimeException("Unable to authenticate data", e); + } catch (NoSuchAlgorithmException e) { + // Should never happen + finalData.release(); + throw new AssertionError(e); + } finally { + dataToForward.release(); } - return buf; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java index 08f58325f..1ee76af1a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; -import com.velocitypowered.proxy.config.IPForwardingMode; +import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.protocol.ProtocolConstants; @@ -97,7 +97,7 @@ public class ServerConnection implements MinecraftConnectionAssociation { Handshake handshake = new Handshake(); handshake.setNextStatus(StateRegistry.LOGIN_ID); handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion()); - if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) { + if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) { handshake.setServerAddress(createBungeeForwardingAddress()); } else { handshake.setServerAddress(serverInfo.getAddress().getHostString()); @@ -111,7 +111,7 @@ public class ServerConnection implements MinecraftConnectionAssociation { // Send the server login packet for <=1.12.2 and for 1.13+ servers not using "modern" forwarding. if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 || - VelocityServer.getServer().getConfiguration().getIpForwardingMode() != IPForwardingMode.MODERN) { + VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() != PlayerInfoForwarding.MODERN) { ServerLogin login = new ServerLogin(); login.setUsername(proxyPlayer.getUsername()); minecraftConnection.write(login); diff --git a/proxy/src/main/resources/velocity.toml b/proxy/src/main/resources/velocity.toml index 18aac1e6d..3bdd006e1 100644 --- a/proxy/src/main/resources/velocity.toml +++ b/proxy/src/main/resources/velocity.toml @@ -19,7 +19,10 @@ online-mode = true # servers using Minecraft 1.12 or lower. # - "modern": Forward player IPs and UUIDs as part of the login process using Velocity's native # forwarding. Only applicable for Minecraft 1.13 or higher. -ip-forwarding = "modern" +player-info-forwarding = "modern" + +# If you are using modern IP forwarding, configure an unique secret here. +player-info-forwarding-secret = "5up3r53cr3t" [servers] # Configure your servers here.