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;
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.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import org.jetbrains.annotations.NotNull;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.util.List;
public class CryptoCodec extends MessageToMessageCodec<ByteBuf, ByteBuf> {
private final Cipher cipherEncode;
private final Cipher cipherDecode;
private SecretKey keyEncode;
private SecretKey keyDecode;
private VelocityCipher encoder;
private VelocityCipher decoder;
public CryptoCodec(@NotNull Cipher cipherEncode, @NotNull Cipher cipherDecode) {
this.cipherEncode = cipherEncode;
this.cipherDecode = cipherDecode;
public CryptoCodec(SecretKey keyEncode, SecretKey keyDecode) {
this.keyEncode = keyEncode;
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
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
var i = msg.readerIndex();
var size = msg.readableBytes();
msg.writerIndex(i + cipherEncode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherEncode.getOutputSize(size))));
out.add(msg.retain());
ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), encoder, msg);
try {
encoder.process(compatible);
out.add(compatible.retain());
} finally {
compatible.release();
}
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
if (!ctx.channel().isActive()) return;
var i = msg.readerIndex();
var size = msg.readableBytes();
msg.writerIndex(i + cipherDecode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherDecode.getOutputSize(size))));
out.add(msg.retain());
ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), decoder, msg);
try {
decoder.process(compatible);
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)
.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)
viaaasLogger.info("Using compression: ${Natives.compress.loadedVariant}, crypto: ${Natives.cipher.loadedVariant}")
viaaasLogger.info("Binded minecraft into " + chFuture!!.sync().channel().localAddress())
}
fun generateCert() {

View File

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

View File

@ -1,8 +1,9 @@
package com.viaversion.aas.config
import com.viaversion.aas.secureRandom
import com.viaversion.viaversion.util.Config
import java.io.File
import java.security.SecureRandom
import java.net.URI
import java.util.*
object VIAaaSConfig : Config(File("config/viaaas.yml")) {
@ -10,25 +11,32 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
reloadConfig()
}
override fun getUnsupportedOptions() = emptyList<String>().toMutableList()
override fun getUnsupportedOptions() = emptyList<String>()
override fun getDefaultConfigURL() = VIAaaSConfig::class.java.classLoader.getResource("viaaas.yml")!!
override fun handleConfig(map: MutableMap<String, Any>) {
// Migration from older config versions
if (map["jwt-secret"]?.toString().isNullOrBlank()) {
map["jwt-secret"] = Base64.getEncoder().encodeToString(ByteArray(64)
.also { SecureRandom().nextBytes(it) })
map["jwt-secret"] = Base64.getEncoder()
.encodeToString(ByteArray(64)
.also { secureRandom.nextBytes(it) })
}
if (map["host-name"] is String) {
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 bindAddress: String get() = this.getString("bind-address", "localhost")!!
val hostName: List<String>
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 useStrongRandom: Boolean get() = this.getBoolean("use-strong-random", true)
val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", 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 }
@ -50,9 +58,6 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
val rateLimitWs: Double get() = this.getDouble("rate-limit-ws", 1.0)
val rateLimitConnectionMc: Double get() = this.getDouble("rate-limit-connection-mc", 10.0)
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
get() = this.getString("jwt-secret", null).let {
if (it.isNullOrBlank()) throw IllegalStateException("invalid jwt-secret") else it
@ -61,4 +66,6 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
val faviconUrl: String?
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 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) {
val user = UserConnectionImpl(ch, true)
ProtocolPipelineImpl(user)
ch.pipeline().also { addSocks5(it) }
ch.pipeline().also { addProxyHandler(it) }
// "crypto"
.addLast("frame", FrameCodec())
// compress

View File

@ -1,5 +1,6 @@
package com.viaversion.aas.handler
import com.viaversion.aas.AspirinServer
import com.viaversion.aas.config.VIAaaSConfig
import com.viaversion.aas.codec.packet.Packet
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.Unpooled
import io.netty.channel.ChannelPipeline
import io.netty.handler.proxy.HttpProxyHandler
import io.netty.handler.proxy.Socks4ProxyHandler
import io.netty.handler.proxy.Socks5ProxyHandler
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 addSocks5(pipe: ChannelPipeline) {
val addr = VIAaaSConfig.backendSocks5ProxyAddress
if (addr != null) {
pipe.addFirst(Socks5ProxyHandler(InetSocketAddress(addr, VIAaaSConfig.backendSocks5ProxyPort)))
fun addProxyHandler(pipe: ChannelPipeline) {
val proxyUri = VIAaaSConfig.backendProxy
if (proxyUri != null) {
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.handler.ConnectionData
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.viaversion.api.protocol.packet.State
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion
@ -44,7 +44,7 @@ object ProtocolDetector {
state = ProtocolDetectionState(future),
frontVer = -1
)
channel.pipeline().also { addSocks5(it) }
channel.pipeline().also { addProxyHandler(it) }
.addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS))
.addLast("frame", FrameCodec())
.addLast("mc", MinecraftCodec())

View File

@ -19,7 +19,6 @@ import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.CompletableFuture
import javax.crypto.Cipher
class LoginState : MinecraftConnectionState {
val callbackPlayerId = CompletableFuture<UUID>()
@ -120,11 +119,9 @@ class LoginState : MinecraftConnectionState {
val cryptoResponse = CryptoResponse()
cryptoResponse.encryptedKey = encryptRsa(backPublicKey, backKey)
cryptoResponse.encryptedToken = encryptRsa(backPublicKey, backToken)
val backAesEn = mcCfb8(backKey, Cipher.ENCRYPT_MODE)
val backAesDe = mcCfb8(backKey, Cipher.DECRYPT_MODE)
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) {
frontHandler.data.frontChannel.pipeline().fireExceptionCaught(e)
}
@ -138,9 +135,8 @@ class LoginState : MinecraftConnectionState {
if (!decryptedToken.contentEquals(frontToken)) throw StacklessException("Invalid verification token!")
val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE)
val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE)
handler.data.frontChannel.pipeline().addBefore("frame", "crypto", CryptoCodec(aesEn, aesDe))
handler.data.frontChannel.pipeline()
.addBefore("frame", "crypto", CryptoCodec(aesKey(frontKey), aesKey(frontKey)))
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
# Address to bind
bind-address: localhost
# Use Netty native transport for Minecraft connections when available.
native-transport-mc: true
# Address of SOCKS5 proxy used for connecting to backend servers. Empty to disable.
# Proxy used to connect to backend servers
# Example: socks5://localhost:9050, socks4://localhost:9050, http://foo:bar@localhost:9080
backend-proxy: ''
# Migrated to backend-proxy
backend-socks5-proxy-address: ''
# Port of SOCKS5 proxy used for connecting to backend servers.
backend-socks5-proxy-port: 9050
#
######
# 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
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
######
# 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.
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:
- viaaas.localhost
- via.localhost
- 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.
# Use -1 to reuse the port sent by client, useful for transparent proxying.
default-backend-port: 25565
@ -48,9 +43,9 @@ default-backend-port: 25565
######
# 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
# 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:
- "*.hypixel.net"
# 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: ''
# 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: 20
#