Vsock stuff for phoenixd

This commit is contained in:
Raymond Sebetoa 2024-08-14 21:44:24 +02:00
parent 26bfc7d3f9
commit a15db45e13
16 changed files with 780 additions and 10 deletions

View File

@ -16,6 +16,8 @@ plugins {
kotlin("multiplatform") version Versions.kotlin kotlin("multiplatform") version Versions.kotlin
kotlin("plugin.serialization") version Versions.kotlin kotlin("plugin.serialization") version Versions.kotlin
id("app.cash.sqldelight") version Versions.sqlDelight id("app.cash.sqldelight") version Versions.sqlDelight
`java-library`
id("org.bytedeco.gradle-javacpp-platform").version("1.5.10")
application application
} }
@ -30,6 +32,7 @@ allprojects {
maven("https://oss.sonatype.org/content/repositories/snapshots") maven("https://oss.sonatype.org/content/repositories/snapshots")
mavenCentral() mavenCentral()
google() google()
maven("https://jitpack.io")
} }
} }
@ -108,10 +111,28 @@ kotlin {
} }
sourceSets { sourceSets {
val jvmMain by getting {
// Include the native source directory if needed
resources.srcDirs("src/commonMain/kotlin/fr/acinq/lightning/vsock/native")
}
commonMain { commonMain {
kotlin.srcDir(buildVersionsTask.map { it.destinationDir }) kotlin.srcDir(buildVersionsTask.map { it.destinationDir })
dependencies { dependencies {
implementation("fr.acinq.lightning:lightning-kmp:${Versions.lightningKmp}") implementation("com.github.raymond98.lightning-kmp:lightning-kmp:v1.6.2-FEECREDIT-8")
//implementation("com.github.raymond98.vsockj:vsockj-native:1.0.3")
//implementation("com.github.raymond98.vsockj:vsockj-core:1.0.3")
implementation("org.bytedeco:javacpp:1.5.10")
implementation(kotlin("stdlib-jdk8"))
implementation("org.bytedeco:javacv-platform:1.5.10")
implementation("org.bytedeco:javacpp-presets:1.5.10")
api("fr.acinq.bitcoin:bitcoin-kmp:${Versions.bitcoinKmpVersion}")
api("co.touchlab:kermit:${Versions.kermitLoggerVersion}")
api("org.jetbrains.kotlinx:kotlinx-datetime:${Versions.datetimeVersion}")
api(ktor("network"))
api(ktor("network-tls"))
// ktor serialization // ktor serialization
implementation(ktor("serialization-kotlinx-json")) implementation(ktor("serialization-kotlinx-json"))
// ktor server // ktor server
@ -135,6 +156,7 @@ kotlin {
jvmMain { jvmMain {
dependencies { dependencies {
implementation("app.cash.sqldelight:sqlite-driver:${Versions.sqlDelight}") implementation("app.cash.sqldelight:sqlite-driver:${Versions.sqlDelight}")
implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:${Versions.secpJniJvmVersion}")
implementation(ktor("client-okhttp")) implementation(ktor("client-okhttp"))
implementation("ch.qos.logback:logback-classic:1.2.3") implementation("ch.qos.logback:logback-classic:1.2.3")
} }
@ -198,6 +220,9 @@ kotlin {
application { application {
mainClass = "fr.acinq.lightning.bin.MainKt" mainClass = "fr.acinq.lightning.bin.MainKt"
// Set java.library.path to include the directory where the shared library is generated
applicationDefaultJvmArgs = listOf("-Djava.library.path=${project.buildDir}/libs")
} }
val cliScripts by tasks.register("cliScripts", CreateStartScripts::class) { val cliScripts by tasks.register("cliScripts", CreateStartScripts::class) {
@ -211,6 +236,33 @@ tasks.startScripts {
dependsOn(cliScripts) dependsOn(cliScripts)
} }
val compileNative by tasks.register<Exec>("compileNative") {
group = "build"
description = "Compile the native C++ code into a shared library"
val outputDir = file("${project.buildDir}/libs")
val nativeSourceDir = file("src/commonMain/kotlin/fr/acinq/lightning/vsock/native")
// Locate the JNI headers - adjust these paths based on your actual JDK location
val jdkHome = System.getenv("JAVA_HOME") ?: "/usr/lib/jvm/default-java"
val jniIncludeDir = file("$jdkHome/include")
val jniPlatformIncludeDir = file("$jniIncludeDir/linux") // or "win32" for Windows, "darwin" for macOS
inputs.dir(nativeSourceDir)
outputs.dir(outputDir)
commandLine("g++", "-I$jniIncludeDir", "-I$jniPlatformIncludeDir", "-shared", "-o", outputDir.resolve("libjniVSockImpl.so"), nativeSourceDir.resolve("VSockImpl.cpp"), "-fPIC")
}
// Ensure the native library is compiled before creating the distribution
tasks.withType<Tar> {
dependsOn(compileNative)
}
tasks.withType<Zip> {
dependsOn(compileNative)
}
distributions { distributions {
main { main {
distributionBaseName = "phoenix" distributionBaseName = "phoenix"
@ -221,6 +273,7 @@ distributions {
// forward std input when app is run via gradle (otherwise keyboard input will return EOF) // forward std input when app is run via gradle (otherwise keyboard input will return EOF)
tasks.withType<JavaExec> { tasks.withType<JavaExec> {
standardInput = System.`in` standardInput = System.`in`
dependsOn(compileNative) //This should not be the case for all platforms
} }
sqldelight { sqldelight {

View File

@ -1,9 +1,16 @@
object Versions { object Versions {
val kotlin = "1.9.23" val kotlin = "1.9.23"
val lightningKmp = "1.7.0-FEECREDIT-8" val lightningKmp = "1.7.0-FEECREDIT-8"
val lightningKmpTag = "v1.6.2-FEECREDIT-8"
val sqlDelight = "2.0.1" val sqlDelight = "2.0.1"
val okio = "3.8.0" val okio = "3.8.0"
val clikt = "4.2.2" val clikt = "4.2.2"
val ktor = "2.3.8" val ktor = "2.3.8"
fun ktor(module: String) = "io.ktor:ktor-$module:$ktor" fun ktor(module: String) = "io.ktor:ktor-$module:$ktor"
//For local tests
val bitcoinKmpVersion = "0.19.0"
val kermitLoggerVersion = "2.0.2"
val datetimeVersion = "0.6.0"
val secpJniJvmVersion = "0.15.0"
} }

View File

@ -337,7 +337,7 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
is Either.Left -> call.respondText(res.value.message.toString()) is Either.Left -> call.respondText(res.value.message.toString())
} }
} }
post("/splicein") {//Manual splice-in post("splicein") {//Manual splice-in
val formParameters = call.receiveParameters() val formParameters = call.receiveParameters()
val amountSat = formParameters.getLong("amountSat").msat //the splice in command will send all the balance in wallet val amountSat = formParameters.getLong("amountSat").msat //the splice in command will send all the balance in wallet
val feerate = FeeratePerKw(FeeratePerByte(formParameters.getLong("feerateSatByte").sat)) val feerate = FeeratePerKw(FeeratePerByte(formParameters.getLong("feerateSatByte").sat))

View File

@ -11,6 +11,7 @@ import com.github.ajalt.clikt.output.MordantHelpFormatter
import com.github.ajalt.clikt.parameters.groups.OptionGroup import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.groups.provideDelegate import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.boolean
import com.github.ajalt.clikt.parameters.types.choice import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.restrictTo import com.github.ajalt.clikt.parameters.types.restrictTo
@ -40,7 +41,6 @@ import fr.acinq.lightning.bin.logs.stringTimestamp
import fr.acinq.lightning.blockchain.electrum.ElectrumClient import fr.acinq.lightning.blockchain.electrum.ElectrumClient
import fr.acinq.lightning.blockchain.electrum.ElectrumWatcher import fr.acinq.lightning.blockchain.electrum.ElectrumWatcher
import fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient import fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient
import fr.acinq.lightning.blockchain.mempool.MempoolSpaceWatcher
import fr.acinq.lightning.crypto.LocalKeyManager import fr.acinq.lightning.crypto.LocalKeyManager
import fr.acinq.lightning.db.ChannelsDb import fr.acinq.lightning.db.ChannelsDb
import fr.acinq.lightning.db.Databases import fr.acinq.lightning.db.Databases
@ -54,6 +54,7 @@ import fr.acinq.lightning.utils.ServerAddress
import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sat
import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toByteVector
import fr.acinq.lightning.vsock.VsockServer
import fr.acinq.phoenix.db.* import fr.acinq.phoenix.db.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
@ -66,7 +67,7 @@ import okio.buffer
import okio.use import okio.use
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes //import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -100,9 +101,9 @@ class Phoenixd : CliktCommand() {
else -> error("unsupported chain") else -> error("unsupported chain")
} }
} }
private val mempoolPollingInterval by option("--mempool-space-polling-interval-minutes", help = "Polling interval for mempool.space API", hidden = true) /*private val mempoolPollingInterval by option("--mempool-space-polling-interval-minutes", help = "Polling interval for mempool.space API", hidden = true)
.int().convert { it.minutes } .int().convert { it.minutes }
.default(10.minutes) .default(10.minutes)*/
private val httpBindIp by option("--http-bind-ip", help = "Bind ip for the http api").default("127.0.0.1") private val httpBindIp by option("--http-bind-ip", help = "Bind ip for the http api").default("127.0.0.1")
private val httpBindPort by option("--http-bind-port", help = "Bind port for the http api").int().default(9740) private val httpBindPort by option("--http-bind-port", help = "Bind port for the http api").int().default(9740)
private val httpPassword by option("--http-password", help = "Password for the http api") private val httpPassword by option("--http-password", help = "Password for the http api")
@ -126,6 +127,14 @@ class Phoenixd : CliktCommand() {
value value
} }
//Electrum has a list of testnet and mainnet servers we can use to randomly find available ones https://github.com/spesmilo/electrum/blob/afa1a4d22a31d23d088c6670e1588eed32f7114d/lib/network.py#L57
private val electrumServerIp by option("--electrum-server-ip", help = "An IP for the Electrum server").default("testnet.qtornado.com")
private val electrumServerPort by option("--electrum-server-port", help = "Port for the electrum server").int().default(51002)
private val startVsock by option("--start-vsock-server", help = "Start the vsock server for API calls").boolean().default(true)
private val vsockCID by option("--vsock-server-cid", help = "CID for the Vsock server").int().default(4)
private val vsockPort by option("--vsock-server-port", help = "Port for the Vsock server").int().default(9001)
class LiquidityOptions : OptionGroup(name = "Liquidity Options") { class LiquidityOptions : OptionGroup(name = "Liquidity Options") {
val autoLiquidity by option("--auto-liquidity", help = "Amount automatically requested when inbound liquidity is needed").choice( val autoLiquidity by option("--auto-liquidity", help = "Amount automatically requested when inbound liquidity is needed").choice(
"off" to 0.sat, "off" to 0.sat,
@ -133,7 +142,7 @@ class Phoenixd : CliktCommand() {
"5m" to 5_000_000.sat, "5m" to 5_000_000.sat,
"10m" to 10_000_000.sat, "10m" to 10_000_000.sat,
).default(2_000_000.sat, "2m") ).default(2_000_000.sat, "2m")
val maxAbsoluteFee by option("--max-absolute-fee", hidden = true).deprecated("--max-absolute-fee is deprecated, use --max-mining-fee instead", error = true) //val maxAbsoluteFee by option("--max-absolute-fee", hidden = true).deprecated("--max-absolute-fee is deprecated, use --max-mining-fee instead", error = true)
val maxMiningFee by option("--max-mining-fee", help = "Max mining fee for on-chain operations, in satoshis") val maxMiningFee by option("--max-mining-fee", help = "Max mining fee for on-chain operations, in satoshis")
.int().convert { it.sat } .int().convert { it.sat }
.restrictTo(5_000.sat..200_000.sat) .restrictTo(5_000.sat..200_000.sat)
@ -276,7 +285,7 @@ class Phoenixd : CliktCommand() {
//val watcher = MempoolSpaceWatcher(mempoolSpace, scope, loggerFactory, pollingInterval = mempoolPollingInterval) //val watcher = MempoolSpaceWatcher(mempoolSpace, scope, loggerFactory, pollingInterval = mempoolPollingInterval)
val electrumClient = ElectrumClient(scope, nodeParams.loggerFactory) val electrumClient = ElectrumClient(scope, nodeParams.loggerFactory)
val serverAddress = ServerAddress("electrum.acinq.co", 50002, TcpSocket.TLS.UNSAFE_CERTIFICATES) val serverAddress = ServerAddress(electrumServerIp, electrumServerPort, TcpSocket.TLS.UNSAFE_CERTIFICATES)
val socketBuilder = TcpSocket.Builder() val socketBuilder = TcpSocket.Builder()
runBlocking { runBlocking {
@ -382,6 +391,12 @@ class Phoenixd : CliktCommand() {
peer.setAutoLiquidityParams(liquidityOptions.autoLiquidity) peer.setAutoLiquidityParams(liquidityOptions.autoLiquidity)
} }
var vsockServer: VsockServer? = null
if(startVsock){
vsockServer = VsockServer(vsockCID, vsockPort, httpBindPort, httpBindIp, loggerFactory)
vsockServer.start()
}
val server = embeddedServer(CIO, port = httpBindPort, host = httpBindIp, val server = embeddedServer(CIO, port = httpBindPort, host = httpBindIp,
configure = { configure = {
reuseAddress = true reuseAddress = true
@ -409,6 +424,7 @@ class Phoenixd : CliktCommand() {
peerConnectionLoop.cancel() peerConnectionLoop.cancel()
peer.disconnect() peer.disconnect()
server.stop() server.stop()
vsockServer?.stop()
exitProcess(0) exitProcess(0)
} }
server.environment.monitor.subscribe(ApplicationStopped) { consoleLog(brightYellow("http server stopped")) } server.environment.monitor.subscribe(ApplicationStopped) { consoleLog(brightYellow("http server stopped")) }

View File

@ -42,9 +42,9 @@ data class LSP(val walletParams: WalletParams, val swapInXpub: String) {
) )
) )
is Chain.Testnet -> LSP( is Chain.Testnet -> LSP(
swapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og", swapInXpub = "tpubDCbLTyj9J59ygFSXyFxssuCFzLLD6ccfQmGdEmwb4miv33NAa7VyRdhDBJRUcsR987cMRm8ufCvdXBphT79QAYWczZJ8mqHeKtrXeE9PVD5",
walletParams = WalletParams( walletParams = WalletParams(
trampolineNode = NodeUri(PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134"), "13.248.222.197", 9735), trampolineNode = NodeUri(PublicKey.fromHex("02cb140e651b9bef52dfe8976ac08336414373d738d161fcf479f98a1ae4416c92"), "10.0.0.137", 9735),
trampolineFees, trampolineFees,
invoiceDefaultRoutingFees, invoiceDefaultRoutingFees,
swapInParams swapInParams

View File

@ -78,6 +78,13 @@ sealed class ApiType {
val deeplyConfirmed: Long val deeplyConfirmed: Long
) : ApiType() ) : ApiType()
@Serializable
data class VsockApiRequest(
val method: String,
val params: Map<String, String> = emptyMap(),
val httpPassword: String
) : ApiType()
@Serializable @Serializable
data class FinalWalletInfo(@SerialName("path") val path: String, @SerialName("xpub") val xpub: String) : ApiType() data class FinalWalletInfo(@SerialName("path") val path: String, @SerialName("xpub") val xpub: String) : ApiType()

View File

@ -0,0 +1,72 @@
package fr.acinq.lightning.vsock
import fr.acinq.lightning.vsock.native.VSockImpl
import java.io.Closeable
import java.io.IOException
import java.net.SocketException
abstract class BaseVSock : Closeable {
protected val closeLock: Any = Any()
protected var isClosed: Boolean = false
protected var created: Boolean = false
protected var bound: Boolean = false
private var implementation: VSockImpl? = null
@Throws(SocketException::class)
private fun createImplementation() {
implementation = VSockImpl()
implementation!!.create()
created = true
}
@Throws(SocketException::class)
fun getImplementation(): VSockImpl? {
if (!created) createImplementation()
return implementation
}
@Throws(SocketException::class)
fun setImplementation(): VSockImpl {
if (implementation == null) {
implementation = VSockImpl()
}
return implementation!!
}
@get:Throws(IOException::class)
val localCid: Int
get() = getImplementation()!!.getLocalCid()
@JvmOverloads
@Throws(IOException::class)
fun bind(address: VSockAddress?, backlog: Int = DEFAULT_BACKLOG) {
var backlog = backlog
if (isClosed) {
throw SocketException("Socket closed")
}
if (bound) {
throw SocketException("Socket already bound")
}
if (backlog <= 0) {
backlog = DEFAULT_BACKLOG
}
getImplementation()!!.bind(address)
getImplementation()!!.listen(backlog)
bound = true
}
@Synchronized
@Throws(IOException::class)
override fun close() {
synchronized(closeLock) {
if (isClosed) return
if (created) getImplementation()!!.close()
isClosed = true
}
}
companion object {
private const val DEFAULT_BACKLOG = 42
}
}

View File

@ -0,0 +1,17 @@
package fr.acinq.lightning.vsock
import java.io.IOException
import java.net.SocketException
class ServerVSock : BaseVSock() {
@Throws(IOException::class)
fun accept(): VSock {
if (isClosed) throw SocketException("Socket closed")
if (!bound) throw SocketException("Socket not bound")
val socket = VSock()
socket.setImplementation()
socket.getImplementation()?.let { getImplementation()!!.accept(it) }
socket.postAccept()
return socket
}
}

View File

@ -0,0 +1,71 @@
package fr.acinq.lightning.vsock
import java.io.Closeable
import java.io.IOException
import java.net.SocketException
class VSock : BaseVSock, Closeable {
private var connected = false
@get:Throws(IOException::class)
@get:Synchronized
var outputStream: VSockOutputStream? = null
get() {
if (isClosed) {
throw SocketException("VSock is closed")
}
if (field == null) {
field = getImplementation()?.let { VSockOutputStream(it) }
}
return field
}
private set
@get:Throws(IOException::class)
@get:Synchronized
var inputStream: VSockInputStream? = null
get() {
if (isClosed) {
throw SocketException("VSock is closed")
}
if (field == null) {
field = getImplementation()?.let { VSockInputStream(it) }
}
return field
}
private set
constructor()
constructor(address: VSockAddress?) {
try {
getImplementation()!!.connect(address)
} catch (e: Exception) {
try {
close()
} catch (ce: Exception) {
e.addSuppressed(ce)
}
throw IllegalStateException(e.message, e)
}
}
@Throws(SocketException::class)
fun connect(address: VSockAddress?) {
if (isClosed) {
throw SocketException("Socket closed")
}
if (connected) {
throw SocketException("Socket already connected")
}
getImplementation()!!.connect(address)
connected = true
bound = true
}
fun postAccept() {
created = true
bound = true
connected = true
}
}

View File

@ -0,0 +1,35 @@
package fr.acinq.lightning.vsock
import java.net.SocketAddress
import java.util.Objects
class VSockAddress(val cid: Int, val port: Int) : SocketAddress() {
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o == null || javaClass != o.javaClass) return false
val that = o as VSockAddress
return cid == that.cid &&
port == that.port
}
override fun hashCode(): Int {
return Objects.hash(cid, port)
}
override fun toString(): String {
return "VSockAddress{" +
"cid=" + cid +
", port=" + port +
'}'
}
companion object {
const val VMADDR_CID_ANY: Int = -1
const val VMADDR_CID_HYPERVISOR: Int = 0
const val VMADDR_CID_RESERVED: Int = 1
const val VMADDR_CID_HOST: Int = 2
const val VMADDR_CID_PARENT: Int = 3
const val VMADDR_PORT_ANY: Int = -1
}
}

View File

@ -0,0 +1,30 @@
package fr.acinq.lightning.vsock
import fr.acinq.lightning.vsock.native.VSockImpl
import java.io.IOException
import java.io.InputStream
class VSockInputStream(private val vSock: VSockImpl) : InputStream() {
private lateinit var temp: ByteArray
@Throws(IOException::class)
override fun read(b: ByteArray, off: Int, len: Int): Int {
return vSock.read(b, off, len)
}
@Throws(IOException::class)
override fun read(): Int {
temp = ByteArray(1)
val n = read(temp, 0, 1)
if (n <= 0) {
return -1
}
return temp[0].toInt()
}
@Throws(IOException::class)
override fun close() {
vSock.close()
super.close()
}
}

View File

@ -0,0 +1,26 @@
package fr.acinq.lightning.vsock
import fr.acinq.lightning.vsock.native.VSockImpl
import java.io.IOException
import java.io.OutputStream
class VSockOutputStream internal constructor(private val vSock: VSockImpl) : OutputStream() {
private val temp = ByteArray(1)
@Throws(IOException::class)
override fun write(b: Int) {
temp[0] = b.toByte()
this.write(temp, 0, 1)
}
@Throws(IOException::class)
override fun write(b: ByteArray, off: Int, len: Int) {
vSock.write(b, off, len)
}
@Throws(IOException::class)
override fun close() {
vSock.close()
super.close()
}
}

View File

@ -0,0 +1,96 @@
package fr.acinq.lightning.vsock
import fr.acinq.lightning.bin.json.ApiType.*
import fr.acinq.lightning.logging.LoggerFactory
import fr.acinq.lightning.logging.debug
import fr.acinq.lightning.logging.error
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.Base64
class VsockServer(private val CID: Int, private val port: Int, httpBindPort: Int, host: String, loggerFactory: LoggerFactory) {
private var server: ServerVSock? = null
private val logger = loggerFactory.newLogger(this::class)
private val client = HttpClient()
private val apiBaseUrl: String = "${host}:${httpBindPort}"
private val bufferSize: Int = 4096
@OptIn(DelicateCoroutinesApi::class)
fun start() {
server = ServerVSock()
try {
server?.bind(VSockAddress(CID, port)) //For any CID use VSockAddress.VMADDR_CID_ANY
logger.debug { "Vsock Bound on Cid: ${server?.localCid}" }
server?.accept()?.use { peerVSock ->
val buffer = ByteArray(bufferSize)
val bytesRead = peerVSock.inputStream?.read(buffer, 0, bufferSize)
if (bytesRead != null) {
if (bytesRead > 0) {
val receivedData = String(buffer, 0, bytesRead, StandardCharsets.UTF_8).trim()
logger.debug { "Received Data: $receivedData" }
// Parse the received data into a http request
val apiRequest = try {
Json.decodeFromString<VsockApiRequest>(receivedData)
} catch (e: Exception) {
logger.error { "Failed to parse JSON: ${e.message}" }
peerVSock.outputStream?.write("Invalid JSON format".toByteArray(StandardCharsets.UTF_8))
return
}
// Handle the API call
GlobalScope.launch {
val response = handleApiCall(apiRequest)
peerVSock.outputStream?.write(response.toByteArray(StandardCharsets.UTF_8))
}
}
}
}
} catch (ex: IOException) {
logger.error { "Error starting Vsock: ${ex.message}" }
} finally {
stop()
}
}
private suspend fun handleApiCall(request: VsockApiRequest): String {
return try {
val url = "$apiBaseUrl/${request.method}"
val response: HttpResponse = client.get(url) {
headers {
append(HttpHeaders.Authorization, "Basic ${Base64.getEncoder().encodeToString("user:${request.httpPassword}".toByteArray())}")
}
url {
parameters.appendAll(Parameters.build {
request.params.forEach { (key, value) ->
append(key, value)
}
})
}
}
if (response.status == HttpStatusCode.OK) {
response.bodyAsText()
} else {
"API Error: ${response.status} - ${response.bodyAsText()}"
}
} catch (e: Exception) {
logger.error { "API call failed: ${e.message}" }
"API call failed: ${e.message}"
}
}
fun stop() {
logger.debug { "Stopping Vsock Server" }
server?.close()
}
}

View File

@ -0,0 +1,195 @@
#include <jni.h>
#include "VSockImpl.h" // Include the generated JNI header
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/vm_sockets.h>
#include <unistd.h>
#include <errno.h>
#define JVM_IO_INTR (-2)
#define BUFFER_LEN 65536
#define min(a, b) ((a) < (b) ? (a) : (b))
// Make sure to wrap all your JNI functions with extern "C" to avoid name mangling
extern "C" {
// Native method implementations matching the JNI header
JNIEXPORT void JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_socketCreate(JNIEnv *env, jobject thisObj) {
int fd = socket(AF_VSOCK, SOCK_STREAM, 0);
// Optionally store the socket descriptor in the Java object's field
}
JNIEXPORT void JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_connect(JNIEnv *env, jobject thisObj, jobject addr) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed");
return;
}
// Get the cid and port from the VSockAddress object
jclass VSockAddressClass = env->GetObjectClass(addr);
jfieldID cidField = env->GetFieldID(VSockAddressClass, "cid", "I");
jfieldID portField = env->GetFieldID(VSockAddressClass, "port", "I");
struct sockaddr_vm sock_addr;
std::memset(&sock_addr, 0, sizeof(struct sockaddr_vm));
sock_addr.svm_family = AF_VSOCK;
sock_addr.svm_port = env->GetIntField(addr, portField);
sock_addr.svm_cid = env->GetIntField(addr, cidField);
int status = ::connect(fd, (struct sockaddr *)&sock_addr, sizeof(struct sockaddr_vm));
if (status != 0) {
env->ThrowNew(env->FindClass("java/net/ConnectException"),
("Connect failed with error no: " + std::to_string(errno)).c_str());
}
}
JNIEXPORT void JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_close(JNIEnv *env, jobject thisObj) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
return;
}
int status = ::close(fd);
fd = -1;
if (status != 0) {
env->ThrowNew(env->FindClass("java/net/SocketException"),
("Close failed with error no: " + std::to_string(errno)).c_str());
}
}
JNIEXPORT void JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_write(JNIEnv *env, jobject thisObj, jbyteArray b, jint offset, jint len) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed");
return;
}
char BUF[BUFFER_LEN];
while (len > 0) {
int chunkLen = min(BUFFER_LEN, len);
env->GetByteArrayRegion(b, offset, chunkLen, (jbyte *)BUF);
int n = (int)::send(fd, BUF, chunkLen, 0);
if (n <= 0) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Write failed");
return;
}
len -= chunkLen;
offset += chunkLen;
}
}
JNIEXPORT jint JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_read(JNIEnv *env, jobject thisObj, jbyteArray b, jint offset, jint len) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed");
return -1;
}
char *bufP = (char *)malloc((size_t)len);
int nread = (int)::recv(fd, bufP, len, 0);
if (nread < 0) {
env->ThrowNew(env->FindClass("java/net/SocketException"),
("Read failed with error no: " + std::to_string(errno)).c_str());
free(bufP);
return -1;
}
env->SetByteArrayRegion(b, offset, nread, (jbyte *)bufP);
free(bufP);
return nread;
}
JNIEXPORT void JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_bind(JNIEnv *env, jobject thisObj, jobject addr) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed");
return;
}
// Get the cid and port from the VSockAddress object
jclass VSockAddressClass = env->GetObjectClass(addr);
jfieldID cidField = env->GetFieldID(VSockAddressClass, "cid", "I");
jfieldID portField = env->GetFieldID(VSockAddressClass, "port", "I");
struct sockaddr_vm sock_addr;
std::memset(&sock_addr, 0, sizeof(struct sockaddr_vm));
sock_addr.svm_family = AF_VSOCK;
sock_addr.svm_port = env->GetIntField(addr, portField);
sock_addr.svm_cid = env->GetIntField(addr, cidField);
int status = ::bind(fd, (struct sockaddr *)&sock_addr, sizeof(struct sockaddr_vm));
if (status != 0) {
env->ThrowNew(env->FindClass("java/net/BindException"),
("Bind failed with error no: " + std::to_string(errno)).c_str());
}
}
JNIEXPORT void JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_listen(JNIEnv *env, jobject thisObj, jint backlog) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed");
return;
}
int status = ::listen(fd, backlog);
if (status != 0) {
env->ThrowNew(env->FindClass("java/net/SocketException"),
("Listen failed with error no: " + std::to_string(errno)).c_str());
}
}
JNIEXPORT void JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_accept(JNIEnv *env, jobject thisObj, jobject connectionVSock) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed");
return;
}
struct sockaddr_vm peer_addr;
socklen_t peer_addr_size = sizeof(struct sockaddr_vm);
int peer_fd = ::accept(fd, (struct sockaddr *)&peer_addr, &peer_addr_size);
if (peer_fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"),
("Accept failed with error no: " + std::to_string(errno)).c_str());
return;
}
// Set the peer_fd in the Java connectionVSock object
jclass VSockImplClass = env->GetObjectClass(connectionVSock);
jfieldID fdField = env->GetFieldID(VSockImplClass, "fd", "I");
env->SetIntField(connectionVSock, fdField, peer_fd);
}
JNIEXPORT jint JNICALL
Java_fr_acinq_lightning_vsock_native_VSockImpl_getLocalCid(JNIEnv *env, jobject thisObj) {
int fd = -1; // Assuming you have stored the fd somewhere accessible
if (fd == -1) {
env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed");
return -1;
}
unsigned int cid;
ioctl(fd, IOCTL_VM_SOCKETS_GET_LOCAL_CID, &cid);
return (jint)cid;
}
} // extern "C"

View File

@ -0,0 +1,86 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class fr_acinq_lightning_vsock_native_VSockImpl */
#ifndef _Included_fr_acinq_lightning_vsock_native_VSockImpl
#define _Included_fr_acinq_lightning_vsock_native_VSockImpl
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: socketCreate
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_socketCreate
(JNIEnv *, jobject);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: connect
* Signature: (Lfr/acinq/lightning/vsock/VSockAddress;)V
*/
JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_connect
(JNIEnv *, jobject, jobject);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: close
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_close
(JNIEnv *, jobject);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: write
* Signature: ([BII)V
*/
JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_write
(JNIEnv *, jobject, jbyteArray, jint, jint);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: read
* Signature: ([BII)I
*/
JNIEXPORT jint JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_read
(JNIEnv *, jobject, jbyteArray, jint, jint);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: bind
* Signature: (Lfr/acinq/lightning/vsock/VSockAddress;)V
*/
JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_bind
(JNIEnv *, jobject, jobject);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: listen
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_listen
(JNIEnv *, jobject, jint);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: accept
* Signature: (Lfr/acinq/lightning/vsock/native/VSockImpl;)V
*/
JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_accept
(JNIEnv *, jobject, jobject);
/*
* Class: fr_acinq_lightning_vsock_native_VSockImpl
* Method: getLocalCid
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_getLocalCid
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,59 @@
package fr.acinq.lightning.vsock.native
import fr.acinq.lightning.vsock.VSockAddress
import org.bytedeco.javacpp.Loader
import org.bytedeco.javacpp.Pointer
import org.bytedeco.javacpp.annotation.Namespace
import org.bytedeco.javacpp.annotation.Platform
import java.net.SocketException
@Platform(include = [
"<sys/socket.h>",
"<sys/ioctl.h>",
"<linux/vm_sockets.h>",
"<unistd.h>",
"<errno.h>"
])
@Namespace("vsock")
class VSockImpl() : Pointer() {
init {
Loader.load() //load the native library
}
var fd: Int = -1
@Throws(SocketException::class)
fun create() {
socketCreate()
}
private external fun allocate()
private external fun socketCreate()
@Throws(Exception::class)
external fun connect(address: VSockAddress?)
@Throws(Exception::class)
external override fun close()
@Throws(Exception::class)
external fun write(b: ByteArray, offset: Int, len: Int)
@Throws(Exception::class)
external fun read(b: ByteArray, offset: Int, len: Int): Int
@Throws(Exception::class)
external fun bind(addr: fr.acinq.lightning.vsock.VSockAddress?)
@Throws(Exception::class)
external fun listen(backlog: Int)
@Throws(Exception::class)
external fun accept(peerVSock: VSockImpl)
@Throws(Exception::class)
external fun getLocalCid(): Int
}