use native velocity crypto, http+socks4 proxy support

This commit is contained in:
creeper123123321 2021-07-02 12:45:53 -03:00
parent 8f94c736b4
commit 8da87cb070
9 changed files with 97 additions and 72 deletions

View File

@ -1,36 +1,60 @@
package com.viaversion.aas.codec; package com.viaversion.aas.codec;
import com.velocitypowered.natives.encryption.VelocityCipher;
import com.velocitypowered.natives.util.MoreByteBufUtils;
import com.velocitypowered.natives.util.Natives;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec; import io.netty.handler.codec.MessageToMessageCodec;
import org.jetbrains.annotations.NotNull;
import javax.crypto.Cipher; import javax.crypto.SecretKey;
import java.util.List; import java.util.List;
public class CryptoCodec extends MessageToMessageCodec<ByteBuf, ByteBuf> { public class CryptoCodec extends MessageToMessageCodec<ByteBuf, ByteBuf> {
private final Cipher cipherEncode; private SecretKey keyEncode;
private final Cipher cipherDecode; private SecretKey keyDecode;
private VelocityCipher encoder;
private VelocityCipher decoder;
public CryptoCodec(@NotNull Cipher cipherEncode, @NotNull Cipher cipherDecode) { public CryptoCodec(SecretKey keyEncode, SecretKey keyDecode) {
this.cipherEncode = cipherEncode; this.keyEncode = keyEncode;
this.cipherDecode = cipherDecode; this.keyDecode = keyDecode;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
encoder = Natives.cipher.get().forEncryption(keyEncode);
decoder = Natives.cipher.get().forDecryption(keyDecode);
keyEncode = null;
keyDecode = null;
} }
@Override @Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
var i = msg.readerIndex(); ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), encoder, msg);
var size = msg.readableBytes(); try {
msg.writerIndex(i + cipherEncode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherEncode.getOutputSize(size)))); encoder.process(compatible);
out.add(msg.retain()); out.add(compatible.retain());
} finally {
compatible.release();
}
} }
@Override @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
if (!ctx.channel().isActive()) return; if (!ctx.channel().isActive()) return;
var i = msg.readerIndex(); ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), decoder, msg);
var size = msg.readableBytes(); try {
msg.writerIndex(i + cipherDecode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherDecode.getOutputSize(size)))); decoder.process(compatible);
out.add(msg.retain()); out.add(compatible.retain());
} finally {
compatible.release();
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
encoder.close();
decoder.close();
} }
} }

View File

@ -103,9 +103,10 @@ object AspirinServer {
.childOption(ChannelOption.TCP_NODELAY, true) .childOption(ChannelOption.TCP_NODELAY, true)
.bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port) .bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port)
viaaasLogger.info("Using compression: ${Natives.compress.loadedVariant}")
viaaasLogger.info("Binded minecraft into " + chFuture!!.sync().channel().localAddress())
ktorServer = embeddedServer(Netty, commandLineEnvironment(args)) {}.start(false) ktorServer = embeddedServer(Netty, commandLineEnvironment(args)) {}.start(false)
viaaasLogger.info("Using compression: ${Natives.compress.loadedVariant}, crypto: ${Natives.cipher.loadedVariant}")
viaaasLogger.info("Binded minecraft into " + chFuture!!.sync().channel().localAddress())
} }
fun generateCert() { fun generateCert() {

View File

@ -7,8 +7,6 @@ import com.google.common.primitives.Ints
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.viaversion.aas.config.VIAaaSConfig import com.viaversion.aas.config.VIAaaSConfig
import com.viaversion.aas.util.StacklessException import com.viaversion.aas.util.StacklessException
import com.viaversion.viaversion.api.Via
import com.viaversion.viaversion.api.protocol.packet.State
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion import com.viaversion.viaversion.api.protocol.version.ProtocolVersion
import com.viaversion.viaversion.api.type.Type import com.viaversion.viaversion.api.type.Type
import io.ktor.client.request.* import io.ktor.client.request.*
@ -45,7 +43,6 @@ import java.security.SecureRandom
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
val badLength = DecoderException("Invalid length!") val badLength = DecoderException("Invalid length!")
@ -53,7 +50,7 @@ val mcLogger = LoggerFactory.getLogger("VIAaaS MC")
val webLogger = LoggerFactory.getLogger("VIAaaS Web") val webLogger = LoggerFactory.getLogger("VIAaaS Web")
val viaaasLogger = LoggerFactory.getLogger("VIAaaS") val viaaasLogger = LoggerFactory.getLogger("VIAaaS")
val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom() val secureRandom = SecureRandom()
suspend fun resolveSrv(hostAndPort: HostAndPort): HostAndPort { suspend fun resolveSrv(hostAndPort: HostAndPort): HostAndPort {
if (hostAndPort.host.endsWith(".onion", ignoreCase = true)) return hostAndPort if (hostAndPort.host.endsWith(".onion", ignoreCase = true)) return hostAndPort
@ -94,14 +91,7 @@ fun encryptRsa(publicKey: PublicKey, data: ByteArray) = Cipher.getInstance("RSA"
it.doFinal(data) it.doFinal(data)
} }
fun mcCfb8(key: ByteArray, mode: Int): Cipher { fun aesKey(key: ByteArray) = SecretKeySpec(key, "AES")
val spec = SecretKeySpec(key, "AES")
val iv = IvParameterSpec(key)
return Cipher.getInstance("AES/CFB8/NoPadding").let {
it.init(mode, spec, iv)
it
}
}
// https://github.com/VelocityPowered/Velocity/blob/6467335f74a7d1617512a55cc9acef5e109b51ac/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java // https://github.com/VelocityPowered/Velocity/blob/6467335f74a7d1617512a55cc9acef5e109b51ac/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
@ -223,11 +213,11 @@ fun sha512Hex(data: ByteArray): String {
} }
fun eventLoopGroup(): EventLoopGroup { fun eventLoopGroup(): EventLoopGroup {
if (VIAaaSConfig.isNativeTransportMc) { return when {
if (Epoll.isAvailable()) return EpollEventLoopGroup() Epoll.isAvailable() -> EpollEventLoopGroup()
if (KQueue.isAvailable()) return KQueueEventLoopGroup() KQueue.isAvailable() -> KQueueEventLoopGroup()
else -> NioEventLoopGroup()
} }
return NioEventLoopGroup()
} }
fun channelServerSocketFactory(eventLoop: EventLoopGroup): ChannelFactory<ServerSocketChannel> { fun channelServerSocketFactory(eventLoop: EventLoopGroup): ChannelFactory<ServerSocketChannel> {

View File

@ -1,8 +1,9 @@
package com.viaversion.aas.config package com.viaversion.aas.config
import com.viaversion.aas.secureRandom
import com.viaversion.viaversion.util.Config import com.viaversion.viaversion.util.Config
import java.io.File import java.io.File
import java.security.SecureRandom import java.net.URI
import java.util.* import java.util.*
object VIAaaSConfig : Config(File("config/viaaas.yml")) { object VIAaaSConfig : Config(File("config/viaaas.yml")) {
@ -10,25 +11,32 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
reloadConfig() reloadConfig()
} }
override fun getUnsupportedOptions() = emptyList<String>().toMutableList() override fun getUnsupportedOptions() = emptyList<String>()
override fun getDefaultConfigURL() = VIAaaSConfig::class.java.classLoader.getResource("viaaas.yml")!! override fun getDefaultConfigURL() = VIAaaSConfig::class.java.classLoader.getResource("viaaas.yml")!!
override fun handleConfig(map: MutableMap<String, Any>) { override fun handleConfig(map: MutableMap<String, Any>) {
// Migration from older config versions
if (map["jwt-secret"]?.toString().isNullOrBlank()) { if (map["jwt-secret"]?.toString().isNullOrBlank()) {
map["jwt-secret"] = Base64.getEncoder().encodeToString(ByteArray(64) map["jwt-secret"] = Base64.getEncoder()
.also { SecureRandom().nextBytes(it) }) .encodeToString(ByteArray(64)
.also { secureRandom.nextBytes(it) })
} }
if (map["host-name"] is String) { if (map["host-name"] is String) {
map["host-name"] = map["host-name"].toString().split(',').map { it.trim() } map["host-name"] = map["host-name"].toString().split(',').map { it.trim() }
} }
val oldSocks = map.remove("backend-socks5-proxy-address")
val oldSocksPort = map.remove("backend-socks5-proxy-port")
if (oldSocks is String && oldSocks.isNotBlank()) {
map["backend-proxy"] = "socks5://$oldSocks:$oldSocksPort"
}
} }
val isNativeTransportMc: Boolean get() = this.getBoolean("native-transport-mc", true)
val port: Int get() = this.getInt("port", 25565) val port: Int get() = this.getInt("port", 25565)
val bindAddress: String get() = this.getString("bind-address", "localhost")!! val bindAddress: String get() = this.getString("bind-address", "localhost")!!
val hostName: List<String> val hostName: List<String>
get() = this.get("host-name", List::class.java, listOf("viaaas.localhost"))!!.map { it.toString() } get() = this.get("host-name", List::class.java, listOf("viaaas.localhost"))!!.map { it.toString() }
val mcRsaSize: Int get() = this.getInt("mc-rsa-size", 4096) val mcRsaSize: Int get() = this.getInt("mc-rsa-size", 4096)
val useStrongRandom: Boolean get() = this.getBoolean("use-strong-random", true)
val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", true) val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", true)
val requireHostName: Boolean get() = this.getBoolean("require-host-name", true) val requireHostName: Boolean get() = this.getBoolean("require-host-name", true)
val defaultBackendPort: Int? get() = this.getInt("default-backend-port", 25565).let { if (it == -1) null else it } val defaultBackendPort: Int? get() = this.getInt("default-backend-port", 25565).let { if (it == -1) null else it }
@ -50,9 +58,6 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
val rateLimitWs: Double get() = this.getDouble("rate-limit-ws", 1.0) val rateLimitWs: Double get() = this.getDouble("rate-limit-ws", 1.0)
val rateLimitConnectionMc: Double get() = this.getDouble("rate-limit-connection-mc", 10.0) val rateLimitConnectionMc: Double get() = this.getDouble("rate-limit-connection-mc", 10.0)
val listeningWsLimit: Int get() = this.getInt("listening-ws-limit", 16) val listeningWsLimit: Int get() = this.getInt("listening-ws-limit", 16)
val backendSocks5ProxyAddress: String?
get() = this.getString("backend-socks5-proxy-address", "")!!.ifEmpty { null }
val backendSocks5ProxyPort: Int get() = this.getInt("backend-socks5-proxy-port", 9050)
val jwtSecret: String val jwtSecret: String
get() = this.getString("jwt-secret", null).let { get() = this.getString("jwt-secret", null).let {
if (it.isNullOrBlank()) throw IllegalStateException("invalid jwt-secret") else it if (it.isNullOrBlank()) throw IllegalStateException("invalid jwt-secret") else it
@ -61,4 +66,6 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
val faviconUrl: String? val faviconUrl: String?
get() = this.getString("favicon-url", "")!!.filter { !it.isWhitespace() }.ifEmpty { null } get() = this.getString("favicon-url", "")!!.filter { !it.isWhitespace() }.ifEmpty { null }
val maxPlayers: Int? get() = this.getInt("max-players", 20).let { if (it == -1) null else it } val maxPlayers: Int? get() = this.getInt("max-players", 20).let { if (it == -1) null else it }
val backendProxy: URI?
get() = this.getString("backend-proxy", "").let { if (it.isNullOrEmpty()) null else URI.create(it) }
} }

View File

@ -13,7 +13,7 @@ class BackEndInit(val connectionData: ConnectionData) : ChannelInitializer<Chann
override fun initChannel(ch: Channel) { override fun initChannel(ch: Channel) {
val user = UserConnectionImpl(ch, true) val user = UserConnectionImpl(ch, true)
ProtocolPipelineImpl(user) ProtocolPipelineImpl(user)
ch.pipeline().also { addSocks5(it) } ch.pipeline().also { addProxyHandler(it) }
// "crypto" // "crypto"
.addLast("frame", FrameCodec()) .addLast("frame", FrameCodec())
// compress // compress

View File

@ -1,5 +1,6 @@
package com.viaversion.aas.handler package com.viaversion.aas.handler
import com.viaversion.aas.AspirinServer
import com.viaversion.aas.config.VIAaaSConfig import com.viaversion.aas.config.VIAaaSConfig
import com.viaversion.aas.codec.packet.Packet import com.viaversion.aas.codec.packet.Packet
import com.viaversion.aas.readRemainingBytes import com.viaversion.aas.readRemainingBytes
@ -9,6 +10,8 @@ import com.viaversion.viaversion.api.type.Type
import io.netty.buffer.ByteBufAllocator import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.ChannelPipeline import io.netty.channel.ChannelPipeline
import io.netty.handler.proxy.HttpProxyHandler
import io.netty.handler.proxy.Socks4ProxyHandler
import io.netty.handler.proxy.Socks5ProxyHandler import io.netty.handler.proxy.Socks5ProxyHandler
import java.net.InetSocketAddress import java.net.InetSocketAddress
@ -18,10 +21,17 @@ fun forward(handler: MinecraftHandler, packet: Packet, flush: Boolean = false) {
fun is17(handler: MinecraftHandler) = handler.data.frontVer!! <= ProtocolVersion.v1_7_6.version fun is17(handler: MinecraftHandler) = handler.data.frontVer!! <= ProtocolVersion.v1_7_6.version
fun addSocks5(pipe: ChannelPipeline) { fun addProxyHandler(pipe: ChannelPipeline) {
val addr = VIAaaSConfig.backendSocks5ProxyAddress val proxyUri = VIAaaSConfig.backendProxy
if (addr != null) { if (proxyUri != null) {
pipe.addFirst(Socks5ProxyHandler(InetSocketAddress(addr, VIAaaSConfig.backendSocks5ProxyPort))) val socket = InetSocketAddress(AspirinServer.dnsResolver.resolve(proxyUri.host).get(), proxyUri.port)
val user = proxyUri.userInfo?.substringBefore(':')
val pass = proxyUri.userInfo?.substringAfter(':')
when (proxyUri.scheme) {
"socks5" -> pipe.addFirst(Socks5ProxyHandler(socket, user, pass))
"socks4" -> pipe.addFirst(Socks4ProxyHandler(socket, user))
"http" -> pipe.addFirst(if (user != null) HttpProxyHandler(socket, user, pass) else HttpProxyHandler(socket))
}
} }
} }

View File

@ -10,7 +10,7 @@ import com.viaversion.aas.codec.packet.handshake.Handshake
import com.viaversion.aas.codec.packet.status.StatusRequest import com.viaversion.aas.codec.packet.status.StatusRequest
import com.viaversion.aas.handler.ConnectionData import com.viaversion.aas.handler.ConnectionData
import com.viaversion.aas.handler.MinecraftHandler import com.viaversion.aas.handler.MinecraftHandler
import com.viaversion.aas.handler.addSocks5 import com.viaversion.aas.handler.addProxyHandler
import com.viaversion.aas.send import com.viaversion.aas.send
import com.viaversion.viaversion.api.protocol.packet.State import com.viaversion.viaversion.api.protocol.packet.State
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion import com.viaversion.viaversion.api.protocol.version.ProtocolVersion
@ -44,7 +44,7 @@ object ProtocolDetector {
state = ProtocolDetectionState(future), state = ProtocolDetectionState(future),
frontVer = -1 frontVer = -1
) )
channel.pipeline().also { addSocks5(it) } channel.pipeline().also { addProxyHandler(it) }
.addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) .addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS))
.addLast("frame", FrameCodec()) .addLast("frame", FrameCodec())
.addLast("mc", MinecraftCodec()) .addLast("mc", MinecraftCodec())

View File

@ -19,7 +19,6 @@ import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import javax.crypto.Cipher
class LoginState : MinecraftConnectionState { class LoginState : MinecraftConnectionState {
val callbackPlayerId = CompletableFuture<UUID>() val callbackPlayerId = CompletableFuture<UUID>()
@ -120,11 +119,9 @@ class LoginState : MinecraftConnectionState {
val cryptoResponse = CryptoResponse() val cryptoResponse = CryptoResponse()
cryptoResponse.encryptedKey = encryptRsa(backPublicKey, backKey) cryptoResponse.encryptedKey = encryptRsa(backPublicKey, backKey)
cryptoResponse.encryptedToken = encryptRsa(backPublicKey, backToken) cryptoResponse.encryptedToken = encryptRsa(backPublicKey, backToken)
val backAesEn = mcCfb8(backKey, Cipher.ENCRYPT_MODE)
val backAesDe = mcCfb8(backKey, Cipher.DECRYPT_MODE)
forward(frontHandler, cryptoResponse, true) forward(frontHandler, cryptoResponse, true)
backChan.pipeline().addBefore("frame", "crypto", CryptoCodec(backAesEn, backAesDe)) backChan.pipeline().addBefore("frame", "crypto", CryptoCodec(aesKey(backKey), aesKey(backKey)))
} catch (e: Exception) { } catch (e: Exception) {
frontHandler.data.frontChannel.pipeline().fireExceptionCaught(e) frontHandler.data.frontChannel.pipeline().fireExceptionCaught(e)
} }
@ -138,9 +135,8 @@ class LoginState : MinecraftConnectionState {
if (!decryptedToken.contentEquals(frontToken)) throw StacklessException("Invalid verification token!") if (!decryptedToken.contentEquals(frontToken)) throw StacklessException("Invalid verification token!")
val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE) handler.data.frontChannel.pipeline()
val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE) .addBefore("frame", "crypto", CryptoCodec(aesKey(frontKey), aesKey(frontKey)))
handler.data.frontChannel.pipeline().addBefore("frame", "crypto", CryptoCodec(aesEn, aesDe))
generateServerHash(frontServerId, frontKey, AspirinServer.mcCryptoKey.public) generateServerHash(frontServerId, frontKey, AspirinServer.mcCryptoKey.public)
} }

View File

@ -4,43 +4,38 @@
####################### #######################
# #
###### ######
# Network # Minecraft networking
###### ######
# Port used for binding Minecraft port # Port used for binding
port: 25565 port: 25565
# Address to bind # Address to bind
bind-address: localhost bind-address: localhost
# Use Netty native transport for Minecraft connections when available. # Proxy used to connect to backend servers
native-transport-mc: true # Example: socks5://localhost:9050, socks4://localhost:9050, http://foo:bar@localhost:9080
# Address of SOCKS5 proxy used for connecting to backend servers. Empty to disable. backend-proxy: ''
# Migrated to backend-proxy
backend-socks5-proxy-address: '' backend-socks5-proxy-address: ''
# Port of SOCKS5 proxy used for connecting to backend servers.
backend-socks5-proxy-port: 9050 backend-socks5-proxy-port: 9050
# #
###### ######
# Crypto # Crypto
###### ######
# Sets the RSA key size used by client for encrypting the AES symmetric key when using online mode. # Sets the RSA key size used for encrypting the AES symmetric key.
# Minecraft default is 1024. See https://stackoverflow.com/questions/1904516/is-1024-bit-rsa-secure # Minecraft default is 1024. See https://stackoverflow.com/questions/1904516/is-1024-bit-rsa-secure
mc-rsa-size: 4096 mc-rsa-size: 4096
# Use SecureRandom.getInstanceStrong(). May block if there's not enough entropy
# See https://wiki.archlinux.org/index.php/Rng-tools
use-strong-random: false
# #
###### ######
# VIAaaS virtual hosts options # VIAaaS virtual hosts options
###### ######
# Requires virtual host to contain the value from "host-name" as a suffix. # Requires virtual host to contain the value from "host-name" as a suffix.
# A false value will allow virtual hosts with no suffix, connecting to the virtual host sent by client. # A false value will allow virtual hosts with no suffix, connecting to the hostname sent by client.
# A false value could be used for transparent proxying or for MiTM. # A false value could be used for transparent proxying or for MiTM.
require-host-name: true require-host-name: true
# Host names of this instance, that will be used in the virtual host as a suffix. # Host names of this instance. Will be used as a suffix.
host-name: host-name:
- viaaas.localhost - viaaas.localhost
- via.localhost - via.localhost
- via.localho.st - via.localho.st
# Requires online mode for front-end connections. May be useful for stopping bots.
force-online-mode: false
# Default port to be used when connecting to the backend server. # Default port to be used when connecting to the backend server.
# Use -1 to reuse the port sent by client, useful for transparent proxying. # Use -1 to reuse the port sent by client, useful for transparent proxying.
default-backend-port: 25565 default-backend-port: 25565
@ -48,9 +43,9 @@ default-backend-port: 25565
###### ######
# Address filtering # Address filtering
###### ######
# Blocks backend connection to local addresses (localhost, 0.0.0.0, ::1, 127.(...), 10.(...), etc). # Blocks backend connection to local addresses (localhost, 0.0.0.0, 10.(...), etc).
block-local-address: true block-local-address: true
# If some server is in this list, it will be blocked. This has priority over allowed-back-addresses. # If some server is in this list, it will be blocked.
blocked-back-addresses: blocked-back-addresses:
- "*.hypixel.net" - "*.hypixel.net"
# Only allows the backend address if it matches an address in this list. # Only allows the backend address if it matches an address in this list.
@ -82,6 +77,8 @@ listening-ws-limit: 10
##### #####
# Favicon URL to use in disconnection messages. Should use "data:image/png;base64," and be a 64x64 PNG # Favicon URL to use in disconnection messages. Should use "data:image/png;base64," and be a 64x64 PNG
favicon-url: '' favicon-url: ''
# Requires online mode for front-end connections. May be useful for stopping bots.
force-online-mode: false
# Max players to allow connection. Use -1 to not limit # Max players to allow connection. Use -1 to not limit
max-players: 20 max-players: 20
# #