diff --git a/README.md b/README.md index f06ec32..51a5c42 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ server. * foundation-bifrost: Discord chat bridge * foundation-chaos: Simulate chaos inside a minecraft world * foundation-heimdall: Event tracking +* foundation-tailscale: Connect the Minecraft Server to Tailscale ## Tools diff --git a/foundation-tailscale/build.gradle.kts b/foundation-tailscale/build.gradle.kts new file mode 100644 index 0000000..ba52419 --- /dev/null +++ b/foundation-tailscale/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("gay.pizza.foundation.concrete-plugin") +} + +repositories { + maven { + name = "GitLabLibtailscale" + url = uri("https://gitlab.com/api/v4/projects/44435887/packages/maven") + } +} + +dependencies { + implementation(project(":common-plugin")) + compileOnly(project(":foundation-shared")) + implementation(libs.tailscale) +} + +concreteItem { + dependency(project(":foundation-core")) +} diff --git a/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/FoundationTailscalePlugin.kt b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/FoundationTailscalePlugin.kt new file mode 100644 index 0000000..4977946 --- /dev/null +++ b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/FoundationTailscalePlugin.kt @@ -0,0 +1,30 @@ +package gay.pizza.foundation.tailscale + +import com.charleskorn.kaml.Yaml +import gay.pizza.foundation.common.BaseFoundationPlugin +import gay.pizza.foundation.common.FoundationCoreLoader +import gay.pizza.foundation.shared.PluginMainClass +import gay.pizza.foundation.shared.copyDefaultConfig +import kotlin.io.path.inputStream + +@PluginMainClass +class FoundationTailscalePlugin : BaseFoundationPlugin() { + lateinit var config: TailscaleConfig + lateinit var controller: TailscaleController + + override fun onEnable() { + val foundation = FoundationCoreLoader.get(server) + val configPath = copyDefaultConfig( + slF4JLogger, + foundation.pluginDataPath, + "tailscale.yaml" + ) + config = Yaml.default.decodeFromStream(TailscaleConfig.serializer(), configPath.inputStream()) + controller = TailscaleController(server, config) + controller.enable() + } + + override fun onDisable() { + controller.disable() + } +} diff --git a/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleConfig.kt b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleConfig.kt new file mode 100644 index 0000000..fea0570 --- /dev/null +++ b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleConfig.kt @@ -0,0 +1,13 @@ +package gay.pizza.foundation.tailscale + +import kotlinx.serialization.Serializable + +@Serializable +data class TailscaleConfig( + val enabled: Boolean = false, + val hostname: String, + val controlUrl: String? = null, + val authKey: String? = null, + val tailscalePath: String? = null, + val ephemeral: Boolean = false +) diff --git a/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleController.kt b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleController.kt new file mode 100644 index 0000000..be1bae0 --- /dev/null +++ b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleController.kt @@ -0,0 +1,38 @@ +package gay.pizza.foundation.tailscale + +import gay.pizza.tailscale.core.Tailscale +import org.bukkit.Server + +class TailscaleController(val server: Server, val config: TailscaleConfig) { + private val tailscale = Tailscale() + + var tailscaleProxyServer: TailscaleProxyServer? = null + + fun enable() { + if (!config.enabled) { + return + } + tailscale.hostname = config.hostname + + if (config.controlUrl != null) { + tailscale.controlUrl = config.controlUrl + } + + if (config.authKey != null) { + tailscale.authKey = config.authKey + } + + if (config.tailscalePath != null) { + tailscale.directoryPath = config.tailscalePath + } + + tailscale.up() + tailscaleProxyServer = TailscaleProxyServer(server, tailscale) + tailscaleProxyServer?.listen() + } + + fun disable() { + tailscaleProxyServer?.close() + tailscale.close() + } +} diff --git a/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleProxyServer.kt b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleProxyServer.kt new file mode 100644 index 0000000..32f4a2a --- /dev/null +++ b/foundation-tailscale/src/main/kotlin/gay/pizza/foundation/tailscale/TailscaleProxyServer.kt @@ -0,0 +1,70 @@ +package gay.pizza.foundation.tailscale + +import gay.pizza.tailscale.core.Tailscale +import gay.pizza.tailscale.core.TailscaleConn +import gay.pizza.tailscale.core.TailscaleListener +import org.bukkit.Server +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException +import java.nio.channels.ReadableByteChannel +import java.nio.channels.SocketChannel +import java.nio.channels.WritableByteChannel + +class TailscaleProxyServer(val server: Server, val tailscale: Tailscale) { + private var minecraftServerListener: TailscaleListener? = null + + fun listen() { + minecraftServerListener?.close() + minecraftServerListener = tailscale.listen("tcp", ":25565") + val thread = Thread { + minecraftServerListener?.threadedAcceptLoop { conn -> + handleServerConnection(conn) + } + } + thread.name = "Tailscale Accept Loop" + thread.start() + } + + fun handleServerConnection(conn: TailscaleConn) { + val socketChannel = SocketChannel.open(InetSocketAddress("127.0.0.1", server.port)) + val connChannel = conn.openReadWriteChannel() + + fun closeAll() { + socketChannel.close() + connChannel.close() + } + + fun startCopyThread(name: String, from: ReadableByteChannel, to: WritableByteChannel) { + val thread = Thread { + try { + while (from.isOpen && to.isOpen) { + val buffer = ByteBuffer.allocate(2048) + val size = from.read(buffer) + if (size < 0) { + break + } else { + buffer.flip() + val array = buffer.array() + to.write(buffer) + } + buffer.clear() + } + } catch (_: ClosedChannelException) { + } finally { + closeAll() + } + } + + thread.name = name + thread.start() + } + + startCopyThread("Tailscale to Socket Pipe", connChannel, socketChannel) + startCopyThread("Socket to Tailscale Pipe", socketChannel, connChannel) + } + + fun close() { + minecraftServerListener?.close() + } +} diff --git a/foundation-tailscale/src/main/resources/plugin.yml b/foundation-tailscale/src/main/resources/plugin.yml new file mode 100644 index 0000000..011ad8d --- /dev/null +++ b/foundation-tailscale/src/main/resources/plugin.yml @@ -0,0 +1,11 @@ +name: Foundation-Tailscale +version: '${version}' +main: gay.pizza.foundation.tailscale.FoundationTailscalePlugin +api-version: 1.18 +prefix: Foundation-Tailscale +load: STARTUP +depend: + - Foundation +authors: + - kubeliv + - azenla diff --git a/foundation-tailscale/src/main/resources/tailscale.yaml b/foundation-tailscale/src/main/resources/tailscale.yaml new file mode 100644 index 0000000..1cbcff9 --- /dev/null +++ b/foundation-tailscale/src/main/resources/tailscale.yaml @@ -0,0 +1,12 @@ +# Whether Tailscale is enabled. +enabled: false +# Hostname for Tailscale node. +hostname: minecraft +# Tailscale control URL. Null for the default. +controlUrl: null +# Tailscale authentication key. Null for the default. +authKey: null +# Tailscale path. Null for the default. +tailscalePath: null +# Tailscale ephemeral mode. +ephemeral: false diff --git a/install.sh b/install.sh index 30f8ad9..446e5b1 100644 --- a/install.sh +++ b/install.sh @@ -41,4 +41,3 @@ do # Download the plugin and store it at the mentioned path. curl --fail -Ls "$base_url/$artifact_path" --output "$dl_path" || (echo "Failed to download ${artifact_path}"; exit 1) done - diff --git a/settings.gradle.kts b/settings.gradle.kts index 9f246e5..b94303b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include( ":foundation-bifrost", ":foundation-chaos", ":foundation-heimdall", + ":foundation-tailscale", ":tool-gjallarhorn", ) @@ -72,6 +73,7 @@ dependencyResolutionManagement { version("postgresql", "42.5.3") version("exposed", "0.41.1") version("hikaricp", "5.0.1") + version("libtailscale", "0.1.2-SNAPSHOT") library("clikt", "com.github.ajalt.clikt", "clikt").versionRef("clikt") library("xodus-core", "org.jetbrains.xodus", "xodus-openAPI").versionRef("xodus") @@ -90,6 +92,7 @@ dependencyResolutionManagement { library("exposed-jdbc", "org.jetbrains.exposed", "exposed-jdbc").versionRef("exposed") library("exposed-java-time", "org.jetbrains.exposed", "exposed-java-time").versionRef("exposed") library("hikaricp", "com.zaxxer", "HikariCP").versionRef("hikaricp") + library("tailscale", "gay.pizza.tailscale", "tailscale").versionRef("libtailscale") } } }