From 793b57c2a14fa2db69dd97b5bfbfae235a2e6df1 Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Sat, 6 Jun 2026 21:52:50 +0800 Subject: [PATCH] feat(ffi): add async data plane API (#2321) * feat(ffi): add async data plane API * feat(ffi): add async data plane examples * test(ffi): make async Go dataplane tests self-contained * docs(ffi): document Go async dataplane API * docs(android): document dataplane JNI API --- .../easytier-android-jni/Cargo.toml | 2 +- .../easytier-android-jni/exports.map | 1 + .../com/easytier/jni/EasyTierDataPlaneJNI.kt | 451 +++++++ .../kotlin/com/easytier/jni/EasyTierJNI.kt | 3 +- .../src/data_plane_api.rs | 673 ++++++++++ .../easytier-android-jni/src/lib.rs | 530 +++++++- .../easytier-android-jni/src/network_api.rs | 6 +- .../examples/example_data_plane_async.c | 429 ++++++ .../easytier-ffi/examples/go/README.md | 52 +- .../examples/go/easytier_async.go | 1018 +++++++++++++++ .../examples/go/easytier_async_test.go | 360 +++++ .../easytier-ffi/examples/go/go.sum | 2 + .../easytier-ffi/src/config_server.rs | 5 +- .../easytier-ffi/src/data_plane.rs | 176 ++- .../easytier-ffi/src/data_plane_async.rs | 1162 +++++++++++++++++ .../easytier-ffi/src/instance_api.rs | 89 +- easytier-contrib/easytier-ffi/src/lib.rs | 238 ++++ easytier-contrib/easytier-ffi/src/tests.rs | 96 ++ 18 files changed, 5219 insertions(+), 74 deletions(-) create mode 100644 easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierDataPlaneJNI.kt create mode 100644 easytier-contrib/easytier-android-jni/src/data_plane_api.rs create mode 100644 easytier-contrib/easytier-ffi/examples/example_data_plane_async.c create mode 100644 easytier-contrib/easytier-ffi/examples/go/easytier_async.go create mode 100644 easytier-contrib/easytier-ffi/examples/go/easytier_async_test.go create mode 100644 easytier-contrib/easytier-ffi/examples/go/go.sum create mode 100644 easytier-contrib/easytier-ffi/src/data_plane_async.rs diff --git a/easytier-contrib/easytier-android-jni/Cargo.toml b/easytier-contrib/easytier-android-jni/Cargo.toml index f810eaac..73212296 100644 --- a/easytier-contrib/easytier-android-jni/Cargo.toml +++ b/easytier-contrib/easytier-android-jni/Cargo.toml @@ -14,4 +14,4 @@ android_logger = "0.13" serde = { version = "1.0.220", features = ["derive"] } serde_json = "1.0" easytier = { path = "../../easytier" } -easytier-ffi = { path = "../easytier-ffi", default-features = false } +easytier-ffi = { path = "../easytier-ffi", default-features = false, features = ["ffi-dataplane"] } diff --git a/easytier-contrib/easytier-android-jni/exports.map b/easytier-contrib/easytier-android-jni/exports.map index 3f2b15f1..9c047617 100644 --- a/easytier-contrib/easytier-android-jni/exports.map +++ b/easytier-contrib/easytier-android-jni/exports.map @@ -1,6 +1,7 @@ { global: Java_com_easytier_jni_EasyTierJNI_*; + Java_com_easytier_jni_EasyTierDataPlaneJNI_*; local: *; }; diff --git a/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierDataPlaneJNI.kt b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierDataPlaneJNI.kt new file mode 100644 index 00000000..8293c6e7 --- /dev/null +++ b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierDataPlaneJNI.kt @@ -0,0 +1,451 @@ +package com.easytier.jni + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withContext + +/** + * EasyTier data-plane API for Android. + * + * Dataplane APIs do not create or start an EasyTier instance by themselves. + * Start an instance with [EasyTierJNI.runNetworkInstance] first, then pass the + * same `instanceName` to [EasyTierDataPlane.tcpConnect], + * [EasyTierDataPlane.tcpBind], or [EasyTierDataPlane.udpBind]. If that instance + * is not running, the native start call fails and the coroutine wrapper throws + * the last EasyTier FFI error. + * + * Typical setup: + * ``` + * val instanceName = "android-dataplane-demo" + * val config = """ + * instance_name = "$instanceName" + * ipv4 = "10.144.0.1" + * listeners = ["tcp://0.0.0.0:11010"] + * + * [network_identity] + * network_name = "android-dataplane-demo" + * network_secret = "replace-with-a-real-secret" + * + * [[peer]] + * uri = "tcp://peer.example.com:11010" + * + * [flags] + * no_tun = true + * bind_device = false + * """.trimIndent() + * + * EasyTierJNI.runNetworkInstance(config) + * ``` + * + * After the instance is running, most callers should use [EasyTierDataPlane] + * and the socket/stream classes below. [EasyTierDataPlaneJNI] is the low-level + * native op-handle ABI used by the coroutine wrappers. + * + * TCP client usage: + * ``` + * val stream = EasyTierDataPlane.tcpConnect(instanceName, "10.144.0.2", 8080, 5_000) + * try { + * stream.write("ping".toByteArray(), 5_000) + * val reply = stream.read(4096, 5_000) + * } finally { + * stream.close() + * } + * ``` + * + * TCP server usage: + * ``` + * val listener = EasyTierDataPlane.tcpBind(instanceName, 8080, 5_000) + * try { + * val stream = listener.accept(30_000) + * try { + * stream.write(stream.read(4096, 5_000), 5_000) + * } finally { + * stream.close() + * } + * } finally { + * listener.close() + * } + * ``` + * + * UDP usage: + * ``` + * val socket = EasyTierDataPlane.udpBind(instanceName, 0, 5_000) + * try { + * socket.sendTo("10.144.0.2", 9000, "ping".toByteArray(), 5_000) + * val packet = socket.recvFrom(4096, 5_000) + * } finally { + * socket.close() + * } + * ``` + * + * Operation model: + * - Each suspend function starts one native async op, waits on Dispatchers.IO, + * then consumes the op with the matching finish call. + * - Coroutine cancellation cancels and frees the native op. + * - Returned stream/listener/socket handles must be closed by the caller. + * - Input ByteArray data is copied by the native start call; output data is + * copied into Kotlin ByteArray before the native buffer is freed. + */ + +/** Data-plane IPv4/port pair returned by EasyTier FFI. */ +data class DataPlaneSocketAddress(val ip: String, val port: Int) + +/** Result of a completed TCP connect op. */ +data class DataPlaneTcpConnectResult(val handle: Long, val localAddress: DataPlaneSocketAddress) + +/** Result of a completed TCP bind op. */ +data class DataPlaneTcpBindResult(val handle: Long, val localAddress: DataPlaneSocketAddress) + +/** Result of a completed TCP accept op. */ +data class DataPlaneTcpAcceptResult( + val handle: Long, + val localAddress: DataPlaneSocketAddress, + val peerAddress: DataPlaneSocketAddress +) + +/** Result of a completed TCP read op. */ +data class DataPlaneTcpReadResult(val data: ByteArray) + +/** Result of a completed UDP bind op. */ +data class DataPlaneUdpBindResult(val handle: Long, val localAddress: DataPlaneSocketAddress) + +/** Result of a completed UDP recv_from op. */ +data class DataPlaneUdpRecvResult( + val data: ByteArray, + val peerAddress: DataPlaneSocketAddress +) + +/** TCP data-plane stream handle. Call [close] when the stream is no longer needed. */ +class DataPlaneTcpStream( + val handle: Long, + val localAddress: DataPlaneSocketAddress? = null, + val peerAddress: DataPlaneSocketAddress? = null +) { + /** Read up to [maxLength] bytes, waiting at most [timeoutMs] in native code. */ + suspend fun read(maxLength: Int, timeoutMs: Long): ByteArray = + EasyTierDataPlane.tcpRead(this, maxLength, timeoutMs) + + /** Write [data], waiting at most [timeoutMs] in native code. */ + suspend fun write(data: ByteArray, timeoutMs: Long): Int = + EasyTierDataPlane.tcpWrite(this, data, timeoutMs) + + /** Close the native TCP stream handle. */ + fun close(): Int = EasyTierDataPlaneJNI.dataPlaneTcpClose(handle) +} + +/** TCP data-plane listener handle. Call [close] when the listener is no longer needed. */ +class DataPlaneTcpListener(val handle: Long, val localAddress: DataPlaneSocketAddress) { + /** Accept one TCP data-plane stream. */ + suspend fun accept(timeoutMs: Long): DataPlaneTcpStream = + EasyTierDataPlane.tcpAccept(this, timeoutMs) + + /** Close the native TCP listener handle. */ + fun close(): Int = EasyTierDataPlaneJNI.dataPlaneTcpListenerClose(handle) +} + +/** UDP data-plane socket handle. Call [close] when the socket is no longer needed. */ +class DataPlaneUdpSocket(val handle: Long, val localAddress: DataPlaneSocketAddress) { + /** Send one UDP datagram to [dstIp]:[dstPort]. */ + suspend fun sendTo( + dstIp: String, + dstPort: Int, + data: ByteArray, + timeoutMs: Long + ): Int = EasyTierDataPlane.udpSendTo(this, dstIp, dstPort, data, timeoutMs) + + /** Receive one UDP datagram and its peer address. */ + suspend fun recvFrom(maxLength: Int, timeoutMs: Long): DataPlaneUdpRecvResult = + EasyTierDataPlane.udpRecvFrom(this, maxLength, timeoutMs) + + /** Close the native UDP socket handle. */ + fun close(): Int = EasyTierDataPlaneJNI.dataPlaneUdpClose(handle) +} + +/** + * Low-level native data-plane JNI entry points. + * + * These functions mirror the Rust FFI op-handle ABI directly. They are exposed + * for completeness, but most Android callers should use [EasyTierDataPlane] + * instead so coroutine cancellation and op cleanup are handled consistently. + */ +object EasyTierDataPlaneJNI { + init { + System.loadLibrary("easytier_android_jni") + } + + @JvmStatic external fun dataPlaneAsyncOpStatus(handle: Long): Int + + @JvmStatic external fun dataPlaneAsyncOpWait(handle: Long, timeoutMs: Long): Int + + @JvmStatic external fun dataPlaneAsyncOpCancel(handle: Long): Int + + @JvmStatic external fun dataPlaneAsyncOpFree(handle: Long): Int + + @JvmStatic + external fun dataPlaneTcpConnectStart( + instanceName: String, + dstIp: String, + dstPort: Int, + timeoutMs: Long + ): Long + + @JvmStatic external fun dataPlaneTcpConnectFinish(op: Long): DataPlaneTcpConnectResult? + + @JvmStatic + external fun dataPlaneTcpBindStart( + instanceName: String, + localPort: Int, + timeoutMs: Long + ): Long + + @JvmStatic external fun dataPlaneTcpBindFinish(op: Long): DataPlaneTcpBindResult? + + @JvmStatic external fun dataPlaneTcpAcceptStart(handle: Long, timeoutMs: Long): Long + + @JvmStatic external fun dataPlaneTcpAcceptFinish(op: Long): DataPlaneTcpAcceptResult? + + @JvmStatic external fun dataPlaneTcpReadStart(handle: Long, maxLength: Int, timeoutMs: Long): Long + + @JvmStatic external fun dataPlaneTcpReadFinish(op: Long): DataPlaneTcpReadResult? + + @JvmStatic external fun dataPlaneTcpWriteStart(handle: Long, data: ByteArray, timeoutMs: Long): Long + + @JvmStatic external fun dataPlaneTcpWriteFinish(op: Long): Int + + @JvmStatic + external fun dataPlaneUdpBindStart( + instanceName: String, + localPort: Int, + timeoutMs: Long + ): Long + + @JvmStatic external fun dataPlaneUdpBindFinish(op: Long): DataPlaneUdpBindResult? + + @JvmStatic + external fun dataPlaneUdpSendToStart( + handle: Long, + dstIp: String, + dstPort: Int, + data: ByteArray, + timeoutMs: Long + ): Long + + @JvmStatic external fun dataPlaneUdpSendToFinish(op: Long): Int + + @JvmStatic external fun dataPlaneUdpRecvFromStart(handle: Long, maxLength: Int, timeoutMs: Long): Long + + @JvmStatic external fun dataPlaneUdpRecvFromFinish(op: Long): DataPlaneUdpRecvResult? + + @JvmStatic external fun dataPlaneTcpClose(handle: Long): Int + + @JvmStatic external fun dataPlaneTcpListenerClose(handle: Long): Int + + @JvmStatic external fun dataPlaneUdpClose(handle: Long): Int +} + +/** Coroutine-friendly Android data-plane API. */ +object EasyTierDataPlane { + private const val DATA_PLANE_OP_PENDING = 0 + private const val DATA_PLANE_OP_READY = 1 + private const val DATA_PLANE_OP_FAILED = -1 + private const val DATA_PLANE_OP_INVALID = -2 + private const val DATA_PLANE_WAIT_SLICE_MS = 50L + + /** Connect to a TCP endpoint through the named EasyTier instance. */ + @JvmStatic + suspend fun tcpConnect( + instanceName: String, + dstIp: String, + dstPort: Int, + timeoutMs: Long + ): DataPlaneTcpStream { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneTcpConnectStart( + instanceName, + dstIp, + dstPort, + timeoutMs + ) + ) + val result = awaitOp(op) { + EasyTierDataPlaneJNI.dataPlaneTcpConnectFinish(it) ?: throw lastDataPlaneException() + } + return DataPlaneTcpStream(result.handle, result.localAddress) + } + + /** Bind a TCP data-plane listener on [localPort]. Port 0 asks EasyTier to allocate one. */ + @JvmStatic + suspend fun tcpBind( + instanceName: String, + localPort: Int, + timeoutMs: Long + ): DataPlaneTcpListener { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneTcpBindStart( + instanceName, + localPort, + timeoutMs + ) + ) + val result = awaitOp(op) { + EasyTierDataPlaneJNI.dataPlaneTcpBindFinish(it) ?: throw lastDataPlaneException() + } + return DataPlaneTcpListener(result.handle, result.localAddress) + } + + /** Accept one TCP stream from [listener]. */ + @JvmStatic + suspend fun tcpAccept(listener: DataPlaneTcpListener, timeoutMs: Long): DataPlaneTcpStream { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneTcpAcceptStart(listener.handle, timeoutMs) + ) + val result = awaitOp(op) { + EasyTierDataPlaneJNI.dataPlaneTcpAcceptFinish(it) ?: throw lastDataPlaneException() + } + return DataPlaneTcpStream(result.handle, result.localAddress, result.peerAddress) + } + + /** Read up to [maxLength] bytes from [stream]. */ + @JvmStatic + suspend fun tcpRead( + stream: DataPlaneTcpStream, + maxLength: Int, + timeoutMs: Long + ): ByteArray { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneTcpReadStart( + stream.handle, + maxLength, + timeoutMs + ) + ) + return awaitOp(op) { + EasyTierDataPlaneJNI.dataPlaneTcpReadFinish(it)?.data + ?: throw lastDataPlaneException() + } + } + + /** Write [data] to [stream]. */ + @JvmStatic + suspend fun tcpWrite(stream: DataPlaneTcpStream, data: ByteArray, timeoutMs: Long): Int { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneTcpWriteStart( + stream.handle, + data, + timeoutMs + ) + ) + return awaitOp(op) { EasyTierDataPlaneJNI.dataPlaneTcpWriteFinish(it) } + } + + /** Bind a UDP data-plane socket on [localPort]. Port 0 asks EasyTier to allocate one. */ + @JvmStatic + suspend fun udpBind( + instanceName: String, + localPort: Int, + timeoutMs: Long + ): DataPlaneUdpSocket { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneUdpBindStart( + instanceName, + localPort, + timeoutMs + ) + ) + val result = awaitOp(op) { + EasyTierDataPlaneJNI.dataPlaneUdpBindFinish(it) ?: throw lastDataPlaneException() + } + return DataPlaneUdpSocket(result.handle, result.localAddress) + } + + /** Send one UDP datagram through [socket]. */ + @JvmStatic + suspend fun udpSendTo( + socket: DataPlaneUdpSocket, + dstIp: String, + dstPort: Int, + data: ByteArray, + timeoutMs: Long + ): Int { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneUdpSendToStart( + socket.handle, + dstIp, + dstPort, + data, + timeoutMs + ) + ) + return awaitOp(op) { EasyTierDataPlaneJNI.dataPlaneUdpSendToFinish(it) } + } + + /** Receive one UDP datagram through [socket]. */ + @JvmStatic + suspend fun udpRecvFrom( + socket: DataPlaneUdpSocket, + maxLength: Int, + timeoutMs: Long + ): DataPlaneUdpRecvResult { + val op = + requireOp( + EasyTierDataPlaneJNI.dataPlaneUdpRecvFromStart( + socket.handle, + maxLength, + timeoutMs + ) + ) + return awaitOp(op) { + EasyTierDataPlaneJNI.dataPlaneUdpRecvFromFinish(it) ?: throw lastDataPlaneException() + } + } + + private fun requireOp(op: Long): Long { + if (op == 0L) { + throw lastDataPlaneException() + } + return op + } + + private suspend fun awaitOp(op: Long, finish: (Long) -> T): T = + withContext(Dispatchers.IO) { + var consumed = false + try { + awaitReady(op) + val result = finish(op) + consumed = true + result + } catch (e: CancellationException) { + EasyTierDataPlaneJNI.dataPlaneAsyncOpCancel(op) + throw e + } finally { + if (!consumed) { + EasyTierDataPlaneJNI.dataPlaneAsyncOpFree(op) + } + } + } + + private suspend fun awaitReady(op: Long) { + while (true) { + currentCoroutineContext().ensureActive() + when (EasyTierDataPlaneJNI.dataPlaneAsyncOpWait(op, DATA_PLANE_WAIT_SLICE_MS)) { + DATA_PLANE_OP_READY, DATA_PLANE_OP_FAILED -> return + DATA_PLANE_OP_PENDING -> Unit + DATA_PLANE_OP_INVALID -> throw RuntimeException("Data-plane async operation is invalid") + else -> throw RuntimeException("Unknown data-plane async operation status") + } + } + } + + private fun lastDataPlaneException(): RuntimeException { + return RuntimeException(EasyTierJNI.getLastError() ?: "EasyTier data-plane call failed") + } +} diff --git a/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt index 9b6995a3..60b28966 100644 --- a/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt +++ b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt @@ -4,9 +4,8 @@ fun interface ConfigServerEventCallback { fun onEvent(eventJson: String) } -/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */ +/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 核心网络功能的接口 */ object EasyTierJNI { - init { // 加载本地库 System.loadLibrary("easytier_android_jni") diff --git a/easytier-contrib/easytier-android-jni/src/data_plane_api.rs b/easytier-contrib/easytier-android-jni/src/data_plane_api.rs new file mode 100644 index 00000000..574054be --- /dev/null +++ b/easytier-contrib/easytier-android-jni/src/data_plane_api.rs @@ -0,0 +1,673 @@ +use std::{ + ffi::{CStr, c_char}, + ptr, +}; + +use easytier_ffi::{ + data_plane_async_op_cancel, data_plane_async_op_free, data_plane_async_op_status, + data_plane_async_op_wait, data_plane_free_bytes, data_plane_tcp_accept_finish, + data_plane_tcp_accept_start, data_plane_tcp_bind_finish, data_plane_tcp_bind_start, + data_plane_tcp_close, data_plane_tcp_connect_finish, data_plane_tcp_connect_start, + data_plane_tcp_listener_close, data_plane_tcp_read_finish, data_plane_tcp_read_start, + data_plane_tcp_write_finish, data_plane_tcp_write_start, data_plane_udp_bind_finish, + data_plane_udp_bind_start, data_plane_udp_close, data_plane_udp_recv_from_finish, + data_plane_udp_recv_from_start, data_plane_udp_send_to_finish, data_plane_udp_send_to_start, + free_string, +}; +use jni::{ + JNIEnv, + objects::{JByteArray, JClass, JObject, JString, JValue}, + sys::{jint, jlong, jobject}, +}; + +use crate::{ + error::{get_last_error, throw_exception}, + strings::jstring_to_cstring, +}; + +const SOCKET_ADDR_CLASS: &str = "com/easytier/jni/DataPlaneSocketAddress"; +const TCP_CONNECT_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpConnectResult"; +const TCP_BIND_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpBindResult"; +const TCP_ACCEPT_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpAcceptResult"; +const TCP_READ_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpReadResult"; +const UDP_BIND_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneUdpBindResult"; +const UDP_RECV_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneUdpRecvResult"; + +fn timeout_from_jlong(timeout_ms: jlong) -> u64 { + timeout_ms.max(0) as u64 +} + +fn port_from_jint(env: &mut JNIEnv, value: jint, name: &str) -> Option { + match u16::try_from(value) { + Ok(port) => Some(port), + Err(_) => { + throw_exception(env, &format!("Invalid {}: {}", name, value)); + None + } + } +} + +fn len_from_jint(env: &mut JNIEnv, value: jint, name: &str) -> Option { + match u32::try_from(value) { + Ok(len) => Some(len), + Err(_) => { + throw_exception(env, &format!("Invalid {}: {}", name, value)); + None + } + } +} + +fn throw_last(env: &mut JNIEnv) { + let message = get_last_error().unwrap_or_else(|| "EasyTier data-plane call failed".to_string()); + throw_exception(env, &message); +} + +unsafe fn take_ffi_string(ptr: *const c_char) -> String { + if ptr.is_null() { + return String::new(); + } + let value = unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned(); + free_string(ptr); + value +} + +fn new_socket_addr<'local>( + env: &mut JNIEnv<'local>, + ip: String, + port: u16, +) -> Option> { + let class = match env.find_class(SOCKET_ADDR_CLASS) { + Ok(class) => class, + Err(err) => { + throw_exception( + env, + &format!("Failed to find socket address class: {:?}", err), + ); + return None; + } + }; + let ip = match env.new_string(ip) { + Ok(ip) => ip, + Err(err) => { + throw_exception(env, &format!("Failed to create IP string: {:?}", err)); + return None; + } + }; + match env.new_object( + class, + "(Ljava/lang/String;I)V", + &[JValue::Object(&ip), JValue::Int(port as jint)], + ) { + Ok(addr) => Some(addr), + Err(err) => { + throw_exception(env, &format!("Failed to create socket address: {:?}", err)); + None + } + } +} + +fn new_handle_addr_result( + env: &mut JNIEnv, + class_name: &str, + handle: u64, + ip: String, + port: u16, +) -> jobject { + let Some(addr) = new_socket_addr(env, ip, port) else { + return ptr::null_mut(); + }; + let class = match env.find_class(class_name) { + Ok(class) => class, + Err(err) => { + throw_exception(env, &format!("Failed to find result class: {:?}", err)); + return ptr::null_mut(); + } + }; + let sig = format!("(JL{};)V", SOCKET_ADDR_CLASS); + match env.new_object( + class, + sig.as_str(), + &[JValue::Long(handle as jlong), JValue::Object(&addr)], + ) { + Ok(result) => result.into_raw(), + Err(err) => { + throw_exception(env, &format!("Failed to create result object: {:?}", err)); + ptr::null_mut() + } + } +} + +fn close_tcp_stream_on_null(result: jobject, handle: u64) -> jobject { + if result.is_null() { + let _ = data_plane_tcp_close(handle); + } + result +} + +fn close_tcp_listener_on_null(result: jobject, handle: u64) -> jobject { + if result.is_null() { + let _ = data_plane_tcp_listener_close(handle); + } + result +} + +fn close_udp_socket_on_null(result: jobject, handle: u64) -> jobject { + if result.is_null() { + let _ = data_plane_udp_close(handle); + } + result +} + +fn read_owned_bytes(ptr: *const u8, len: u32) -> Vec { + if ptr.is_null() || len == 0 { + return Vec::new(); + } + let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec(); + data_plane_free_bytes(ptr, len); + bytes +} + +pub(crate) fn async_op_status_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint { + data_plane_async_op_status(handle as u64) +} + +pub(crate) fn async_op_wait_jni( + _env: JNIEnv, + _class: JClass, + handle: jlong, + timeout_ms: jlong, +) -> jint { + data_plane_async_op_wait(handle as u64, timeout_ms.max(0) as u64) +} + +pub(crate) fn async_op_cancel_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint { + data_plane_async_op_cancel(handle as u64) +} + +pub(crate) fn async_op_free_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint { + data_plane_async_op_free(handle as u64) +} + +pub(crate) fn tcp_connect_start_jni( + mut env: JNIEnv, + _class: JClass, + inst_name: JString, + dst_ip: JString, + dst_port: jint, + timeout_ms: jlong, +) -> jlong { + let inst_name = match jstring_to_cstring(&mut env, &inst_name) { + Ok(value) => value, + Err(err) => { + throw_exception(&mut env, &format!("Invalid instance name: {}", err)); + return 0; + } + }; + let dst_ip = match jstring_to_cstring(&mut env, &dst_ip) { + Ok(value) => value, + Err(err) => { + throw_exception(&mut env, &format!("Invalid destination IP: {}", err)); + return 0; + } + }; + let Some(dst_port) = port_from_jint(&mut env, dst_port, "destination port") else { + return 0; + }; + let op = unsafe { + data_plane_tcp_connect_start( + inst_name.as_ptr(), + dst_ip.as_ptr(), + dst_port, + timeout_ms.max(0) as u64, + ) + }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn tcp_connect_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject { + let mut ip: *const c_char = ptr::null(); + let mut port = 0u16; + let handle = unsafe { data_plane_tcp_connect_finish(op as u64, &mut ip, &mut port) }; + if handle == 0 { + throw_last(&mut env); + return ptr::null_mut(); + } + close_tcp_stream_on_null( + new_handle_addr_result( + &mut env, + TCP_CONNECT_RESULT_CLASS, + handle, + unsafe { take_ffi_string(ip) }, + port, + ), + handle, + ) +} + +pub(crate) fn tcp_bind_start_jni( + mut env: JNIEnv, + _class: JClass, + inst_name: JString, + local_port: jint, + timeout_ms: jlong, +) -> jlong { + let inst_name = match jstring_to_cstring(&mut env, &inst_name) { + Ok(value) => value, + Err(err) => { + throw_exception(&mut env, &format!("Invalid instance name: {}", err)); + return 0; + } + }; + let Some(local_port) = port_from_jint(&mut env, local_port, "local port") else { + return 0; + }; + let op = unsafe { + data_plane_tcp_bind_start( + inst_name.as_ptr(), + local_port, + timeout_from_jlong(timeout_ms), + ) + }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn tcp_bind_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject { + let mut ip: *const c_char = ptr::null(); + let mut port = 0u16; + let handle = unsafe { data_plane_tcp_bind_finish(op as u64, &mut ip, &mut port) }; + if handle == 0 { + throw_last(&mut env); + return ptr::null_mut(); + } + close_tcp_listener_on_null( + new_handle_addr_result( + &mut env, + TCP_BIND_RESULT_CLASS, + handle, + unsafe { take_ffi_string(ip) }, + port, + ), + handle, + ) +} + +pub(crate) fn tcp_accept_start_jni( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + timeout_ms: jlong, +) -> jlong { + let op = unsafe { data_plane_tcp_accept_start(handle as u64, timeout_from_jlong(timeout_ms)) }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn tcp_accept_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject { + let mut local_ip: *const c_char = ptr::null(); + let mut local_port = 0u16; + let mut peer_ip: *const c_char = ptr::null(); + let mut peer_port = 0u16; + let handle = unsafe { + data_plane_tcp_accept_finish( + op as u64, + &mut local_ip, + &mut local_port, + &mut peer_ip, + &mut peer_port, + ) + }; + if handle == 0 { + throw_last(&mut env); + return ptr::null_mut(); + } + let Some(local_addr) = + new_socket_addr(&mut env, unsafe { take_ffi_string(local_ip) }, local_port) + else { + free_string(peer_ip); + let _ = data_plane_tcp_close(handle); + return ptr::null_mut(); + }; + let Some(peer_addr) = new_socket_addr(&mut env, unsafe { take_ffi_string(peer_ip) }, peer_port) + else { + let _ = data_plane_tcp_close(handle); + return ptr::null_mut(); + }; + let class = match env.find_class(TCP_ACCEPT_RESULT_CLASS) { + Ok(class) => class, + Err(err) => { + throw_exception( + &mut env, + &format!("Failed to find accept result class: {:?}", err), + ); + let _ = data_plane_tcp_close(handle); + return ptr::null_mut(); + } + }; + let sig = format!("(JL{};L{};)V", SOCKET_ADDR_CLASS, SOCKET_ADDR_CLASS); + let result = match env.new_object( + class, + sig.as_str(), + &[ + JValue::Long(handle as jlong), + JValue::Object(&local_addr), + JValue::Object(&peer_addr), + ], + ) { + Ok(result) => result.into_raw(), + Err(err) => { + throw_exception( + &mut env, + &format!("Failed to create accept result: {:?}", err), + ); + ptr::null_mut() + } + }; + close_tcp_stream_on_null(result, handle) +} + +pub(crate) fn tcp_read_start_jni( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + max_len: jint, + timeout_ms: jlong, +) -> jlong { + let Some(max_len) = len_from_jint(&mut env, max_len, "max length") else { + return 0; + }; + let op = unsafe { + data_plane_tcp_read_start(handle as u64, max_len, timeout_from_jlong(timeout_ms)) + }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn tcp_read_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject { + let mut ptr: *const u8 = ptr::null(); + let mut len = 0u32; + let ret = unsafe { data_plane_tcp_read_finish(op as u64, &mut ptr, &mut len) }; + if ret < 0 { + throw_last(&mut env); + return ptr::null_mut(); + } + let bytes = read_owned_bytes(ptr, len); + let array = match env.byte_array_from_slice(&bytes) { + Ok(array) => array, + Err(err) => { + throw_exception(&mut env, &format!("Failed to create byte array: {:?}", err)); + return ptr::null_mut(); + } + }; + let class = match env.find_class(TCP_READ_RESULT_CLASS) { + Ok(class) => class, + Err(err) => { + throw_exception( + &mut env, + &format!("Failed to find read result class: {:?}", err), + ); + return ptr::null_mut(); + } + }; + match env.new_object(class, "([B)V", &[JValue::Object(&array)]) { + Ok(result) => result.into_raw(), + Err(err) => { + throw_exception( + &mut env, + &format!("Failed to create read result: {:?}", err), + ); + ptr::null_mut() + } + } +} + +pub(crate) fn tcp_write_start_jni( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + data: JByteArray, + timeout_ms: jlong, +) -> jlong { + let data = match env.convert_byte_array(&data) { + Ok(data) => data, + Err(err) => { + throw_exception(&mut env, &format!("Invalid write buffer: {:?}", err)); + return 0; + } + }; + let ptr = if data.is_empty() { + ptr::null() + } else { + data.as_ptr() + }; + let op = unsafe { + data_plane_tcp_write_start( + handle as u64, + ptr, + data.len() as u32, + timeout_from_jlong(timeout_ms), + ) + }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn tcp_write_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jint { + let ret = data_plane_tcp_write_finish(op as u64); + if ret < 0 { + throw_last(&mut env); + } + ret +} + +pub(crate) fn udp_bind_start_jni( + mut env: JNIEnv, + _class: JClass, + inst_name: JString, + local_port: jint, + timeout_ms: jlong, +) -> jlong { + let inst_name = match jstring_to_cstring(&mut env, &inst_name) { + Ok(value) => value, + Err(err) => { + throw_exception(&mut env, &format!("Invalid instance name: {}", err)); + return 0; + } + }; + let Some(local_port) = port_from_jint(&mut env, local_port, "local port") else { + return 0; + }; + let op = unsafe { + data_plane_udp_bind_start( + inst_name.as_ptr(), + local_port, + timeout_from_jlong(timeout_ms), + ) + }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn udp_bind_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject { + let mut ip: *const c_char = ptr::null(); + let mut port = 0u16; + let handle = unsafe { data_plane_udp_bind_finish(op as u64, &mut ip, &mut port) }; + if handle == 0 { + throw_last(&mut env); + return ptr::null_mut(); + } + close_udp_socket_on_null( + new_handle_addr_result( + &mut env, + UDP_BIND_RESULT_CLASS, + handle, + unsafe { take_ffi_string(ip) }, + port, + ), + handle, + ) +} + +pub(crate) fn udp_send_to_start_jni( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + dst_ip: JString, + dst_port: jint, + data: JByteArray, + timeout_ms: jlong, +) -> jlong { + let dst_ip = match jstring_to_cstring(&mut env, &dst_ip) { + Ok(value) => value, + Err(err) => { + throw_exception(&mut env, &format!("Invalid destination IP: {}", err)); + return 0; + } + }; + let Some(dst_port) = port_from_jint(&mut env, dst_port, "destination port") else { + return 0; + }; + let data = match env.convert_byte_array(&data) { + Ok(data) => data, + Err(err) => { + throw_exception(&mut env, &format!("Invalid UDP send buffer: {:?}", err)); + return 0; + } + }; + let ptr = if data.is_empty() { + ptr::null() + } else { + data.as_ptr() + }; + let op = unsafe { + data_plane_udp_send_to_start( + handle as u64, + dst_ip.as_ptr(), + dst_port, + ptr, + data.len() as u32, + timeout_from_jlong(timeout_ms), + ) + }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn udp_send_to_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jint { + let ret = data_plane_udp_send_to_finish(op as u64); + if ret < 0 { + throw_last(&mut env); + } + ret +} + +pub(crate) fn udp_recv_from_start_jni( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + max_len: jint, + timeout_ms: jlong, +) -> jlong { + let Some(max_len) = len_from_jint(&mut env, max_len, "max length") else { + return 0; + }; + let op = unsafe { + data_plane_udp_recv_from_start(handle as u64, max_len, timeout_from_jlong(timeout_ms)) + }; + if op == 0 { + throw_last(&mut env); + } + op as jlong +} + +pub(crate) fn udp_recv_from_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject { + let mut ptr: *const u8 = ptr::null(); + let mut len = 0u32; + let mut ip: *const c_char = ptr::null(); + let mut port = 0u16; + let ret = unsafe { + data_plane_udp_recv_from_finish(op as u64, &mut ptr, &mut len, &mut ip, &mut port) + }; + if ret < 0 { + throw_last(&mut env); + return ptr::null_mut(); + } + let bytes = read_owned_bytes(ptr, len); + let array = match env.byte_array_from_slice(&bytes) { + Ok(array) => array, + Err(err) => { + free_string(ip); + throw_exception(&mut env, &format!("Failed to create byte array: {:?}", err)); + return ptr::null_mut(); + } + }; + let Some(peer_addr) = new_socket_addr(&mut env, unsafe { take_ffi_string(ip) }, port) else { + return ptr::null_mut(); + }; + let class = match env.find_class(UDP_RECV_RESULT_CLASS) { + Ok(class) => class, + Err(err) => { + throw_exception( + &mut env, + &format!("Failed to find UDP recv result class: {:?}", err), + ); + return ptr::null_mut(); + } + }; + let sig = format!("([BL{};)V", SOCKET_ADDR_CLASS); + match env.new_object( + class, + sig.as_str(), + &[JValue::Object(&array), JValue::Object(&peer_addr)], + ) { + Ok(result) => result.into_raw(), + Err(err) => { + throw_exception( + &mut env, + &format!("Failed to create UDP recv result: {:?}", err), + ); + ptr::null_mut() + } + } +} + +pub(crate) fn tcp_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint { + let ret = data_plane_tcp_close(handle as u64); + if ret != 0 { + throw_last(&mut env); + } + ret +} + +pub(crate) fn tcp_listener_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint { + let ret = data_plane_tcp_listener_close(handle as u64); + if ret != 0 { + throw_last(&mut env); + } + ret +} + +pub(crate) fn udp_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint { + let ret = data_plane_udp_close(handle as u64); + if ret != 0 { + throw_last(&mut env); + } + ret +} diff --git a/easytier-contrib/easytier-android-jni/src/lib.rs b/easytier-contrib/easytier-android-jni/src/lib.rs index 7760b0ae..309f4963 100644 --- a/easytier-contrib/easytier-android-jni/src/lib.rs +++ b/easytier-contrib/easytier-android-jni/src/lib.rs @@ -19,17 +19,22 @@ //! //! Error API: //! - `getLastError()`: return the latest FFI/JNI error string for the calling thread. +//! +//! Data-plane APIs: +//! - `EasyTierDataPlaneJNI.*`: low-level async op-handle data-plane JNI. +//! - `EasyTierJNI.dataPlane*`: compatibility exports for older callers. mod callback; mod config_server_api; +mod data_plane_api; mod error; mod logger; mod network_api; mod strings; use jni::JNIEnv; -use jni::objects::{JClass, JObject, JObjectArray, JString}; -use jni::sys::{jboolean, jint, jstring}; +use jni::objects::{JByteArray, JClass, JObject, JObjectArray, JString}; +use jni::sys::{jboolean, jint, jlong, jobject, jstring}; /// Attach a TUN file descriptor to an EasyTier network instance. /// @@ -91,7 +96,7 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance( /// `EasyTierJNI.retainNetworkInstance(instanceNames: Array?): Int` /// /// Passing `null` or an empty array stops all instances. Null elements inside a -/// non-empty array are skipped. On failure this returns `-1` and throws +/// non-empty array are invalid. On failure this returns `-1` and throws /// `RuntimeException`. #[unsafe(no_mangle)] pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance( @@ -201,3 +206,522 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_isConfigServerClientCon logger::init(); config_server_api::is_config_server_client_connected_jni(env, class) } + +macro_rules! export_data_plane_jni { + ( + $op_status:ident, + $op_wait:ident, + $op_cancel:ident, + $op_free:ident, + $tcp_connect_start:ident, + $tcp_connect_finish:ident, + $tcp_bind_start:ident, + $tcp_bind_finish:ident, + $tcp_accept_start:ident, + $tcp_accept_finish:ident, + $tcp_read_start:ident, + $tcp_read_finish:ident, + $tcp_write_start:ident, + $tcp_write_finish:ident, + $udp_bind_start:ident, + $udp_bind_finish:ident, + $udp_send_to_start:ident, + $udp_send_to_finish:ident, + $udp_recv_from_start:ident, + $udp_recv_from_finish:ident, + $tcp_close:ident, + $tcp_listener_close:ident, + $udp_close:ident + ) => { + #[unsafe(no_mangle)] + pub extern "system" fn $op_status(env: JNIEnv, class: JClass, handle: jlong) -> jint { + logger::init(); + data_plane_api::async_op_status_jni(env, class, handle) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $op_wait( + env: JNIEnv, + class: JClass, + handle: jlong, + timeout_ms: jlong, + ) -> jint { + logger::init(); + data_plane_api::async_op_wait_jni(env, class, handle, timeout_ms) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $op_cancel(env: JNIEnv, class: JClass, handle: jlong) -> jint { + logger::init(); + data_plane_api::async_op_cancel_jni(env, class, handle) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $op_free(env: JNIEnv, class: JClass, handle: jlong) -> jint { + logger::init(); + data_plane_api::async_op_free_jni(env, class, handle) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_connect_start( + env: JNIEnv, + class: JClass, + inst_name: JString, + dst_ip: JString, + dst_port: jint, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::tcp_connect_start_jni( + env, class, inst_name, dst_ip, dst_port, timeout_ms, + ) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_connect_finish( + env: JNIEnv, + class: JClass, + op: jlong, + ) -> jobject { + logger::init(); + data_plane_api::tcp_connect_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_bind_start( + env: JNIEnv, + class: JClass, + inst_name: JString, + local_port: jint, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::tcp_bind_start_jni(env, class, inst_name, local_port, timeout_ms) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_bind_finish(env: JNIEnv, class: JClass, op: jlong) -> jobject { + logger::init(); + data_plane_api::tcp_bind_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_accept_start( + env: JNIEnv, + class: JClass, + handle: jlong, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::tcp_accept_start_jni(env, class, handle, timeout_ms) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_accept_finish( + env: JNIEnv, + class: JClass, + op: jlong, + ) -> jobject { + logger::init(); + data_plane_api::tcp_accept_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_read_start( + env: JNIEnv, + class: JClass, + handle: jlong, + max_len: jint, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::tcp_read_start_jni(env, class, handle, max_len, timeout_ms) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_read_finish(env: JNIEnv, class: JClass, op: jlong) -> jobject { + logger::init(); + data_plane_api::tcp_read_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_write_start( + env: JNIEnv, + class: JClass, + handle: jlong, + data: JByteArray, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::tcp_write_start_jni(env, class, handle, data, timeout_ms) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_write_finish(env: JNIEnv, class: JClass, op: jlong) -> jint { + logger::init(); + data_plane_api::tcp_write_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $udp_bind_start( + env: JNIEnv, + class: JClass, + inst_name: JString, + local_port: jint, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::udp_bind_start_jni(env, class, inst_name, local_port, timeout_ms) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $udp_bind_finish(env: JNIEnv, class: JClass, op: jlong) -> jobject { + logger::init(); + data_plane_api::udp_bind_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $udp_send_to_start( + env: JNIEnv, + class: JClass, + handle: jlong, + dst_ip: JString, + dst_port: jint, + data: JByteArray, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::udp_send_to_start_jni( + env, class, handle, dst_ip, dst_port, data, timeout_ms, + ) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $udp_send_to_finish(env: JNIEnv, class: JClass, op: jlong) -> jint { + logger::init(); + data_plane_api::udp_send_to_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $udp_recv_from_start( + env: JNIEnv, + class: JClass, + handle: jlong, + max_len: jint, + timeout_ms: jlong, + ) -> jlong { + logger::init(); + data_plane_api::udp_recv_from_start_jni(env, class, handle, max_len, timeout_ms) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $udp_recv_from_finish( + env: JNIEnv, + class: JClass, + op: jlong, + ) -> jobject { + logger::init(); + data_plane_api::udp_recv_from_finish_jni(env, class, op) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_close(env: JNIEnv, class: JClass, handle: jlong) -> jint { + logger::init(); + data_plane_api::tcp_close_jni(env, class, handle) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $tcp_listener_close( + env: JNIEnv, + class: JClass, + handle: jlong, + ) -> jint { + logger::init(); + data_plane_api::tcp_listener_close_jni(env, class, handle) + } + + #[unsafe(no_mangle)] + pub extern "system" fn $udp_close(env: JNIEnv, class: JClass, handle: jlong) -> jint { + logger::init(); + data_plane_api::udp_close_jni(env, class, handle) + } + }; +} + +export_data_plane_jni!( + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpStatus, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpWait, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpCancel, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpFree, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpConnectStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpConnectFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpBindStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpBindFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpAcceptStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpAcceptFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpReadStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpReadFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpWriteStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpWriteFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpBindStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpBindFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpSendToStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpSendToFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpRecvFromStart, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpRecvFromFinish, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpClose, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpListenerClose, + Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpClose +); + +// Compatibility exports for older Kotlin/Java callers that used EasyTierJNI +// directly for data-plane operations. + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpStatus( + env: JNIEnv, + class: JClass, + handle: jlong, +) -> jint { + logger::init(); + data_plane_api::async_op_status_jni(env, class, handle) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpWait( + env: JNIEnv, + class: JClass, + handle: jlong, + timeout_ms: jlong, +) -> jint { + logger::init(); + data_plane_api::async_op_wait_jni(env, class, handle, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpCancel( + env: JNIEnv, + class: JClass, + handle: jlong, +) -> jint { + logger::init(); + data_plane_api::async_op_cancel_jni(env, class, handle) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpFree( + env: JNIEnv, + class: JClass, + handle: jlong, +) -> jint { + logger::init(); + data_plane_api::async_op_free_jni(env, class, handle) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpConnectStart( + env: JNIEnv, + class: JClass, + inst_name: JString, + dst_ip: JString, + dst_port: jint, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::tcp_connect_start_jni(env, class, inst_name, dst_ip, dst_port, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpConnectFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jobject { + logger::init(); + data_plane_api::tcp_connect_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpBindStart( + env: JNIEnv, + class: JClass, + inst_name: JString, + local_port: jint, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::tcp_bind_start_jni(env, class, inst_name, local_port, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpBindFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jobject { + logger::init(); + data_plane_api::tcp_bind_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpAcceptStart( + env: JNIEnv, + class: JClass, + handle: jlong, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::tcp_accept_start_jni(env, class, handle, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpAcceptFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jobject { + logger::init(); + data_plane_api::tcp_accept_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpReadStart( + env: JNIEnv, + class: JClass, + handle: jlong, + max_len: jint, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::tcp_read_start_jni(env, class, handle, max_len, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpReadFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jobject { + logger::init(); + data_plane_api::tcp_read_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpWriteStart( + env: JNIEnv, + class: JClass, + handle: jlong, + data: JByteArray, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::tcp_write_start_jni(env, class, handle, data, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpWriteFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jint { + logger::init(); + data_plane_api::tcp_write_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpBindStart( + env: JNIEnv, + class: JClass, + inst_name: JString, + local_port: jint, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::udp_bind_start_jni(env, class, inst_name, local_port, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpBindFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jobject { + logger::init(); + data_plane_api::udp_bind_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpSendToStart( + env: JNIEnv, + class: JClass, + handle: jlong, + dst_ip: JString, + dst_port: jint, + data: JByteArray, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::udp_send_to_start_jni(env, class, handle, dst_ip, dst_port, data, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpSendToFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jint { + logger::init(); + data_plane_api::udp_send_to_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpRecvFromStart( + env: JNIEnv, + class: JClass, + handle: jlong, + max_len: jint, + timeout_ms: jlong, +) -> jlong { + logger::init(); + data_plane_api::udp_recv_from_start_jni(env, class, handle, max_len, timeout_ms) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpRecvFromFinish( + env: JNIEnv, + class: JClass, + op: jlong, +) -> jobject { + logger::init(); + data_plane_api::udp_recv_from_finish_jni(env, class, op) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpClose( + env: JNIEnv, + class: JClass, + handle: jlong, +) -> jint { + logger::init(); + data_plane_api::tcp_close_jni(env, class, handle) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpListenerClose( + env: JNIEnv, + class: JClass, + handle: jlong, +) -> jint { + logger::init(); + data_plane_api::tcp_listener_close_jni(env, class, handle) +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpClose( + env: JNIEnv, + class: JClass, + handle: jlong, +) -> jint { + logger::init(); + data_plane_api::udp_close_jni(env, class, handle) +} diff --git a/easytier-contrib/easytier-android-jni/src/network_api.rs b/easytier-contrib/easytier-android-jni/src/network_api.rs index b7300b86..4d3a8998 100644 --- a/easytier-contrib/easytier-android-jni/src/network_api.rs +++ b/easytier-contrib/easytier-android-jni/src/network_api.rs @@ -113,7 +113,11 @@ pub(crate) fn retain_network_instance_jni( }; if java_string.is_null() { - continue; + throw_exception( + &mut env, + &format!("Invalid instance name at index {}: null", i), + ); + return -1; } let jstring = JString::from(java_string); diff --git a/easytier-contrib/easytier-ffi/examples/example_data_plane_async.c b/easytier-contrib/easytier-ffi/examples/example_data_plane_async.c new file mode 100644 index 00000000..24f3bd9d --- /dev/null +++ b/easytier-contrib/easytier-ffi/examples/example_data_plane_async.c @@ -0,0 +1,429 @@ +#include +#include +#include +#include + +#define DATA_PLANE_OP_PENDING 0 +#define DATA_PLANE_OP_READY 1 +#define DATA_PLANE_OP_FAILED -1 +#define DATA_PLANE_OP_INVALID -2 + +extern int run_network_instance(const char *cfg_str); +extern void get_error_msg(const char **out); +extern void free_string(const char *s); + +extern int data_plane_async_op_status(uint64_t op); +extern int data_plane_async_op_wait(uint64_t op, uint64_t timeout_ms); +extern int data_plane_async_op_cancel(uint64_t op); +extern int data_plane_async_op_free(uint64_t op); +extern void data_plane_free_bytes(const uint8_t *ptr, uint32_t len); + +extern uint64_t data_plane_tcp_connect_start( + const char *inst_name, + const char *dst_ip, + uint16_t dst_port, + uint64_t timeout_ms); +extern uint64_t data_plane_tcp_connect_finish( + uint64_t op, + const char **out_local_ip, + uint16_t *out_local_port); +extern uint64_t data_plane_tcp_bind_start( + const char *inst_name, + uint16_t local_port, + uint64_t timeout_ms); +extern uint64_t data_plane_tcp_bind_finish( + uint64_t op, + const char **out_local_ip, + uint16_t *out_local_port); +extern uint64_t data_plane_tcp_accept_start(uint64_t listener, uint64_t timeout_ms); +extern uint64_t data_plane_tcp_accept_finish( + uint64_t op, + const char **out_local_ip, + uint16_t *out_local_port, + const char **out_peer_ip, + uint16_t *out_peer_port); +extern uint64_t data_plane_tcp_read_start( + uint64_t stream, + uint32_t max_len, + uint64_t timeout_ms); +extern int data_plane_tcp_read_finish( + uint64_t op, + const uint8_t **out_buf, + uint32_t *out_len); +extern uint64_t data_plane_tcp_write_start( + uint64_t stream, + const uint8_t *buf, + uint32_t len, + uint64_t timeout_ms); +extern int data_plane_tcp_write_finish(uint64_t op); +extern int data_plane_tcp_close(uint64_t stream); +extern int data_plane_tcp_listener_close(uint64_t listener); + +extern uint64_t data_plane_udp_bind_start( + const char *inst_name, + uint16_t local_port, + uint64_t timeout_ms); +extern uint64_t data_plane_udp_bind_finish( + uint64_t op, + const char **out_local_ip, + uint16_t *out_local_port); +extern uint64_t data_plane_udp_send_to_start( + uint64_t socket, + const char *dst_ip, + uint16_t dst_port, + const uint8_t *buf, + uint32_t len, + uint64_t timeout_ms); +extern int data_plane_udp_send_to_finish(uint64_t op); +extern uint64_t data_plane_udp_recv_from_start( + uint64_t socket, + uint32_t max_len, + uint64_t timeout_ms); +extern int data_plane_udp_recv_from_finish( + uint64_t op, + const uint8_t **out_buf, + uint32_t *out_len, + const char **out_ip, + uint16_t *out_port); +extern int data_plane_udp_close(uint64_t socket); + +static void print_last_error(const char *prefix) { + const char *err = NULL; + get_error_msg(&err); + if (err) { + fprintf(stderr, "%s: %s\n", prefix, err); + free_string(err); + } else { + fprintf(stderr, "%s\n", prefix); + } +} + +static int parse_ip_port(const char *value, char *ip, size_t ip_len, uint16_t *port) { + const char *colon = strrchr(value, ':'); + if (!colon || colon == value || !colon[1]) { + fprintf(stderr, "expected IPv4 target in IP:PORT form, got %s\n", value); + return -1; + } + size_t host_len = (size_t)(colon - value); + if (host_len >= ip_len) { + fprintf(stderr, "IP address is too long: %s\n", value); + return -1; + } + char *end = NULL; + long parsed_port = strtol(colon + 1, &end, 10); + if (!end || *end != '\0' || parsed_port < 0 || parsed_port > 65535) { + fprintf(stderr, "invalid port in %s\n", value); + return -1; + } + memcpy(ip, value, host_len); + ip[host_len] = '\0'; + *port = (uint16_t)parsed_port; + return 0; +} + +static int wait_op(uint64_t op, uint64_t timeout_ms) { + uint64_t waited = 0; + while (waited < timeout_ms) { + int status = data_plane_async_op_wait(op, 50); + if (status != DATA_PLANE_OP_PENDING) { + return status; + } + waited += 50; + } + return data_plane_async_op_status(op); +} + +static int wait_or_cancel(uint64_t op, uint64_t timeout_ms, const char *what) { + int status = wait_op(op, timeout_ms); + if (status == DATA_PLANE_OP_READY || status == DATA_PLANE_OP_FAILED) { + return status; + } + if (status == DATA_PLANE_OP_PENDING) { + fprintf(stderr, "%s did not finish within %llu ms\n", what, (unsigned long long)timeout_ms); + data_plane_async_op_cancel(op); + data_plane_async_op_free(op); + return DATA_PLANE_OP_INVALID; + } + fprintf(stderr, "%s returned invalid op status %d\n", what, status); + return status; +} + +static int async_tcp_read_once(uint64_t stream, uint64_t timeout_ms) { + uint64_t op = data_plane_tcp_read_start(stream, 512, timeout_ms); + if (!op) { + print_last_error("tcp read start failed"); + return -1; + } + if (wait_or_cancel(op, timeout_ms + 1000, "tcp read") == DATA_PLANE_OP_INVALID) { + return -1; + } + + const uint8_t *buf = NULL; + uint32_t len = 0; + int ret = data_plane_tcp_read_finish(op, &buf, &len); + if (ret < 0) { + print_last_error("tcp read finish failed"); + return -1; + } + printf("tcp read %d bytes: %.*s\n", ret, ret, buf ? (const char *)buf : ""); + data_plane_free_bytes(buf, len); + return 0; +} + +static int async_tcp_write_all(uint64_t stream, const char *data, uint64_t timeout_ms) { + uint64_t op = data_plane_tcp_write_start( + stream, + (const uint8_t *)data, + (uint32_t)strlen(data), + timeout_ms); + if (!op) { + print_last_error("tcp write start failed"); + return -1; + } + if (wait_or_cancel(op, timeout_ms + 1000, "tcp write") == DATA_PLANE_OP_INVALID) { + return -1; + } + int ret = data_plane_tcp_write_finish(op); + if (ret < 0) { + print_last_error("tcp write finish failed"); + return -1; + } + printf("tcp wrote %d bytes\n", ret); + return 0; +} + +static int run_tcp_connect_demo(const char *inst, const char *target) { + char ip[128]; + uint16_t port = 0; + if (parse_ip_port(target, ip, sizeof(ip), &port) != 0) { + return -1; + } + + uint64_t op = data_plane_tcp_connect_start(inst, ip, port, 30000); + if (!op) { + print_last_error("tcp connect start failed"); + return -1; + } + if (wait_or_cancel(op, 31000, "tcp connect") == DATA_PLANE_OP_INVALID) { + return -1; + } + + const char *local_ip = NULL; + uint16_t local_port = 0; + uint64_t stream = data_plane_tcp_connect_finish(op, &local_ip, &local_port); + if (!stream) { + print_last_error("tcp connect finish failed"); + return -1; + } + printf("tcp connected from %s:%u to %s:%u, handle=%llu\n", + local_ip, + local_port, + ip, + port, + (unsigned long long)stream); + free_string(local_ip); + + int ret = async_tcp_read_once(stream, 10000); + data_plane_tcp_close(stream); + return ret; +} + +static int run_tcp_listen_demo(const char *inst, const char *port_text) { + uint16_t port = (uint16_t)strtoul(port_text, NULL, 10); + uint64_t op = data_plane_tcp_bind_start(inst, port, 30000); + if (!op) { + print_last_error("tcp bind start failed"); + return -1; + } + if (wait_or_cancel(op, 31000, "tcp bind") == DATA_PLANE_OP_INVALID) { + return -1; + } + + const char *local_ip = NULL; + uint16_t local_port = 0; + uint64_t listener = data_plane_tcp_bind_finish(op, &local_ip, &local_port); + if (!listener) { + print_last_error("tcp bind finish failed"); + return -1; + } + printf("tcp listening on %s:%u, handle=%llu\n", + local_ip, + local_port, + (unsigned long long)listener); + free_string(local_ip); + + op = data_plane_tcp_accept_start(listener, 60000); + if (!op) { + print_last_error("tcp accept start failed"); + data_plane_tcp_listener_close(listener); + return -1; + } + if (wait_or_cancel(op, 61000, "tcp accept") == DATA_PLANE_OP_INVALID) { + data_plane_tcp_listener_close(listener); + return -1; + } + + const char *peer_ip = NULL; + uint16_t peer_port = 0; + local_ip = NULL; + local_port = 0; + uint64_t stream = data_plane_tcp_accept_finish( + op, + &local_ip, + &local_port, + &peer_ip, + &peer_port); + data_plane_tcp_listener_close(listener); + if (!stream) { + print_last_error("tcp accept finish failed"); + return -1; + } + printf("tcp accepted %s:%u -> %s:%u, stream=%llu\n", + peer_ip, + peer_port, + local_ip, + local_port, + (unsigned long long)stream); + free_string(local_ip); + free_string(peer_ip); + + int ret = async_tcp_read_once(stream, 10000); + if (ret == 0) { + ret = async_tcp_write_all(stream, "pong", 10000); + } + data_plane_tcp_close(stream); + return ret; +} + +static int run_udp_demo(const char *inst, const char *target) { + char ip[128]; + uint16_t port = 0; + if (parse_ip_port(target, ip, sizeof(ip), &port) != 0) { + return -1; + } + + uint64_t op = data_plane_udp_bind_start(inst, 0, 30000); + if (!op) { + print_last_error("udp bind start failed"); + return -1; + } + if (wait_or_cancel(op, 31000, "udp bind") == DATA_PLANE_OP_INVALID) { + return -1; + } + + const char *local_ip = NULL; + uint16_t local_port = 0; + uint64_t socket = data_plane_udp_bind_finish(op, &local_ip, &local_port); + if (!socket) { + print_last_error("udp bind finish failed"); + return -1; + } + printf("udp bound on %s:%u, handle=%llu\n", + local_ip, + local_port, + (unsigned long long)socket); + free_string(local_ip); + + const char payload[] = "ping"; + op = data_plane_udp_send_to_start( + socket, + ip, + port, + (const uint8_t *)payload, + (uint32_t)strlen(payload), + 10000); + if (!op) { + print_last_error("udp send start failed"); + data_plane_udp_close(socket); + return -1; + } + if (wait_or_cancel(op, 11000, "udp send") == DATA_PLANE_OP_INVALID) { + data_plane_udp_close(socket); + return -1; + } + int sent = data_plane_udp_send_to_finish(op); + if (sent < 0) { + print_last_error("udp send finish failed"); + data_plane_udp_close(socket); + return -1; + } + printf("udp sent %d bytes to %s:%u\n", sent, ip, port); + + op = data_plane_udp_recv_from_start(socket, 512, 30000); + if (!op) { + print_last_error("udp recv start failed"); + data_plane_udp_close(socket); + return -1; + } + if (wait_or_cancel(op, 31000, "udp recv") == DATA_PLANE_OP_INVALID) { + data_plane_udp_close(socket); + return -1; + } + + const uint8_t *buf = NULL; + uint32_t len = 0; + const char *peer_ip = NULL; + uint16_t peer_port = 0; + int ret = data_plane_udp_recv_from_finish(op, &buf, &len, &peer_ip, &peer_port); + if (ret < 0) { + print_last_error("udp recv finish failed"); + data_plane_udp_close(socket); + return -1; + } + printf("udp received %d bytes from %s:%u: %.*s\n", + ret, + peer_ip, + peer_port, + ret, + buf ? (const char *)buf : ""); + data_plane_free_bytes(buf, len); + free_string(peer_ip); + data_plane_udp_close(socket); + return 0; +} + +static void print_usage(void) { + printf("Set EASYTIER_FFI_CONFIG and EASYTIER_FFI_INSTANCE to run the async data-plane demo.\n"); + printf("Optional demos:\n"); + printf(" EASYTIER_FFI_TARGET=10.0.0.2:22 async TCP connect/read\n"); + printf(" EASYTIER_FFI_LISTEN_PORT=12345 async TCP bind/accept/read/write\n"); + printf(" EASYTIER_FFI_UDP_TARGET=10.0.0.2:9000 async UDP bind/send_to/recv_from\n"); +} + +int main(void) { + const char *config = getenv("EASYTIER_FFI_CONFIG"); + const char *instance = getenv("EASYTIER_FFI_INSTANCE"); + if (!config || !instance) { + print_usage(); + return 0; + } + + if (run_network_instance(config) != 0) { + print_last_error("run_network_instance failed"); + return 1; + } + printf("network instance started: %s\n", instance); + + int failed = 0; + const char *target = getenv("EASYTIER_FFI_TARGET"); + if (target) { + failed |= run_tcp_connect_demo(instance, target) != 0; + } + + const char *listen_port = getenv("EASYTIER_FFI_LISTEN_PORT"); + if (listen_port) { + failed |= run_tcp_listen_demo(instance, listen_port) != 0; + } + + const char *udp_target = getenv("EASYTIER_FFI_UDP_TARGET"); + if (udp_target) { + failed |= run_udp_demo(instance, udp_target) != 0; + } + + if (!target && !listen_port && !udp_target) { + printf("No dataplane demo env var was set; nothing else to run.\n"); + print_usage(); + } + + return failed ? 1 : 0; +} diff --git a/easytier-contrib/easytier-ffi/examples/go/README.md b/easytier-contrib/easytier-ffi/examples/go/README.md index e3ad4607..9db93dbe 100644 --- a/easytier-contrib/easytier-ffi/examples/go/README.md +++ b/easytier-contrib/easytier-ffi/examples/go/README.md @@ -3,6 +3,8 @@ This demo wraps EasyTier FFI data-plane TCP as Go `net.Conn` and `net.Listener`. It can connect to an SSH server through EasyTier and read its banner, or accept a TCP connection from another EasyTier peer and run a small ping/pong exchange. +The async op-handle wrapper is in `easytier_async.go`; the original synchronous +wrapper stays in `easytier.go`. ## 1.1. Build the FFI library @@ -27,7 +29,6 @@ To use another library path, export `EASYTIER_FFI_LIB=/path/to/libeasytier_ffi.s ```sh export EASYTIER_FFI_CONFIG='instance_name = "default" ipv4 = "10.0.0.1" -peers = ["tcp://123.123.123.123:11010"] [network_identity] network_name = "testnet" @@ -35,6 +36,10 @@ network_secret = "mysecret" [flags] no_tun = true # disable tun device to avoid permission issues. +bind_device = false # allow loopback peers in local examples. + +[[peer]] +uri = "tcp://123.123.123.123:11010" ' ``` @@ -53,7 +58,6 @@ test, use a separate instance name and config: ```sh export EASYTIER_FFI_LISTEN_CONFIG='instance_name = "listener" ipv4 = "10.0.0.3" -peers = ["tcp://123.123.123.123:11010"] [network_identity] network_name = "testnet" @@ -61,6 +65,10 @@ network_secret = "mysecret" [flags] no_tun = true +bind_device = false + +[[peer]] +uri = "tcp://123.123.123.123:11010" ' export EASYTIER_FFI_LISTEN_INSTANCE=listener export EASYTIER_FFI_LISTEN_PORT=12345 @@ -68,14 +76,27 @@ export EASYTIER_FFI_LISTEN_PORT=12345 ## 1.3. Run the demo -`goffi` is built without cgo on Linux, so run the test with `CGO_ENABLED=0`: +`goffi` is built without cgo on Linux, so run the tests with `CGO_ENABLED=0`: ```sh cd easytier-contrib/easytier-ffi/examples/go CGO_ENABLED=0 go test -v ./... ``` -Expected output includes an SSH banner similar to: +The synchronous tests use the environment variables above. The async Go tests +are self-contained: they start two local EasyTier instances in the same test +process with `no_tun = true` and `bind_device = false`, then run TCP and UDP +ping/pong over the async data-plane API. + +To run only the async tests: + +```sh +cd easytier-contrib/easytier-ffi/examples/go +CGO_ENABLED=0 go test -run 'TestAsync' -v ./... +``` + +When the SSH integration environment variables are set, expected synchronous +test output includes an SSH banner similar to: ```text attempt 1: got banner "SSH-2.0-..." @@ -85,3 +106,26 @@ PASS For `TestTCPListenIntegration`, connect from another EasyTier peer to the local EasyTier IPv4 address and `EASYTIER_FFI_LISTEN_PORT`, send `ping`, and expect `pong` in response. + +The async test output should include local TCP bind/connect log lines and finish +with `PASS` without any extra environment variables. + +## 1.4. C async example + +The C async example is kept separate from the basic C example: + +```sh +cargo build -p easytier-ffi --features ffi-dataplane +cc -Wall -Wextra -pedantic \ + ../example_data_plane_async.c \ + -L ../../../../target/debug -leasytier_ffi \ + -Wl,-rpath,../../../../target/debug \ + -o /tmp/easytier_data_plane_async + +/tmp/easytier_data_plane_async +``` + +Without environment variables it prints usage and exits successfully. With +`EASYTIER_FFI_CONFIG`, `EASYTIER_FFI_INSTANCE`, and one of +`EASYTIER_FFI_TARGET`, `EASYTIER_FFI_LISTEN_PORT`, or `EASYTIER_FFI_UDP_TARGET`, +it runs the corresponding async data-plane flow. diff --git a/easytier-contrib/easytier-ffi/examples/go/easytier_async.go b/easytier-contrib/easytier-ffi/examples/go/easytier_async.go new file mode 100644 index 00000000..232b9667 --- /dev/null +++ b/easytier-contrib/easytier-ffi/examples/go/easytier_async.go @@ -0,0 +1,1018 @@ +// Package easytierffi contains a small Go wrapper around the EasyTier FFI +// examples. This file documents the async data-plane surface; the synchronous +// wrapper lives in easytier.go. +// +// Public async entry points: +// +// - OpenAsync(path) loads the EasyTier FFI dynamic library and binds the +// async dataplane symbols. Close releases only the dynamic library handle; +// network instances started through RunNetworkInstance are process-global +// EasyTier state. +// +// - (*AsyncNative).RunNetworkInstance(config) starts one EasyTier instance +// from TOML. The instance name in the config is used by all dataplane calls. +// +// - (*AsyncNative).DialContext(ctx, instance, "tcp", "ip:port") starts an +// async TCP connect and returns an AsyncConn implementing net.Conn. +// +// - (*AsyncNative).ListenContext(ctx, instance, "tcp", "0.0.0.0:port") starts +// an async TCP bind and returns an AsyncListener implementing net.Listener. +// +// - AsyncConn implements net.Conn. Read and Write each start one native async +// read/write op and wait for completion. Deadlines are mapped to operation +// timeouts. Close closes the underlying dataplane stream handle. +// +// - AsyncListener implements net.Listener. Accept starts one native async +// accept op and waits for a stream. Close closes the listener handle. +// +// - (*AsyncNative).UDPBindContext(ctx, instance, port) returns an +// AsyncUDPSocket. AsyncUDPSocket.SendTo and RecvFrom start one native async +// UDP send/receive op and wait for completion. Close closes the socket +// handle. +// +// - TCPConnectContext/TCPBindContext/TCPAcceptContext/TCPReadContext/ +// TCPWriteContext and UDPSendToContext/UDPRecvFromContext are lower-level +// handle helpers used by the examples and tests. External callers should +// prefer DialContext, ListenContext, AsyncConn, AsyncListener, and +// AsyncUDPSocket because raw handle close helpers are intentionally internal +// to this example package. +// +// Async operation semantics: +// +// - Each Context method starts a native async op, polls data_plane_async_op_wait +// in short intervals, then calls the matching finish function. Finish is +// single-consume on the native side. +// +// - If the context is canceled or its deadline expires before completion, the +// wrapper cancels and frees the native op and returns the context error. +// +// - Read and RecvFrom copy Rust-owned output buffers into Go slices and free +// the native allocation before returning. +// +// - Write and SendTo keep the Go input buffer alive for the start call. The +// native async API copies the input buffer during start, so callers do not +// need to keep it alive after the Go method returns. +// +// - FFI calls that read the Rust thread-local error string pin the goroutine +// to one OS thread from the failing call through get_error_msg. +package easytierffi + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "runtime" + "strings" + "sync/atomic" + "time" + "unsafe" + + "github.com/go-webgpu/goffi/ffi" + "github.com/go-webgpu/goffi/types" +) + +const ( + dataPlaneOpPending = int32(0) + dataPlaneOpReady = int32(1) + dataPlaneOpFailed = int32(-1) + dataPlaneOpInvalid = int32(-2) + + asyncPollInterval = 50 * time.Millisecond +) + +type AsyncNative struct { + lib unsafe.Pointer + + runNetworkInstance symCall + deleteNetworkInst symCall + getErrorMsg symCall + freeString symCall + freeBytes symCall + + asyncOpStatus symCall + asyncOpWait symCall + asyncOpCancel symCall + asyncOpFree symCall + + tcpConnectStart symCall + tcpConnectFinish symCall + tcpBindStart symCall + tcpBindFinish symCall + tcpAcceptStart symCall + tcpAcceptFinish symCall + tcpReadStart symCall + tcpReadFinish symCall + tcpWriteStart symCall + tcpWriteFinish symCall + tcpClose symCall + tcpListenerClose symCall + + udpBindStart symCall + udpBindFinish symCall + udpSendToStart symCall + udpSendToFinish symCall + udpRecvFromStart symCall + udpRecvFromFinish symCall + udpClose symCall +} + +type AsyncConn struct { + native *AsyncNative + handle uint64 + local net.Addr + remote net.Addr + closed atomicBool + rd atomicDeadline + wd atomicDeadline +} + +type AsyncListener struct { + native *AsyncNative + handle uint64 + addr net.Addr + closed atomicBool +} + +type AsyncUDPSocket struct { + native *AsyncNative + handle uint64 + addr *net.UDPAddr + closed atomicBool +} + +type atomicBool struct{ v atomic.Bool } + +func OpenAsync(path string) (*AsyncNative, error) { + lib, err := ffi.LoadLibrary(path) + if err != nil { + return nil, err + } + n := &AsyncNative{lib: lib} + if err := n.bind(); err != nil { + ffi.FreeLibrary(lib) + return nil, err + } + return n, nil +} + +func (n *AsyncNative) Close() error { + if n.lib == nil { + return nil + } + ffi.FreeLibrary(n.lib) + n.lib = nil + return nil +} + +func (n *AsyncNative) RunNetworkInstance(config string) error { + defer pinErrorThread()() + cfg := cString(config) + cfgPtr := unsafe.Pointer(&cfg[0]) + var ret int32 + err := n.runNetworkInstance.call(unsafe.Pointer(&ret), unsafe.Pointer(&cfgPtr)) + runtime.KeepAlive(cfg) + if err != nil { + return err + } + if ret != 0 { + return n.lastError() + } + return nil +} + +func (n *AsyncNative) deleteNetworkInstances(names []string) error { + defer pinErrorThread()() + + cNames := make([][]byte, len(names)) + namePtrs := make([]unsafe.Pointer, len(names)) + for i, name := range names { + cNames[i] = cString(name) + namePtrs[i] = unsafe.Pointer(&cNames[i][0]) + } + + var namesPtr unsafe.Pointer + if len(namePtrs) > 0 { + namesPtr = unsafe.Pointer(&namePtrs[0]) + } + length := uint64(len(names)) + var ret int32 + err := n.deleteNetworkInst.call(unsafe.Pointer(&ret), unsafe.Pointer(&namesPtr), unsafe.Pointer(&length)) + runtime.KeepAlive(cNames) + runtime.KeepAlive(namePtrs) + if err != nil { + return err + } + if ret != 0 { + return n.lastError() + } + return nil +} + +func (n *AsyncNative) DialContext(ctx context.Context, instance, network, address string) (net.Conn, error) { + if network != "tcp" && network != "tcp4" && network != "tcp6" { + return nil, net.UnknownNetworkError(network) + } + ip, port, err := parseIPPort(address) + if err != nil { + return nil, err + } + handle, local, err := n.TCPConnectContext(ctx, instance, ip.String(), uint16(port)) + if err != nil { + return nil, err + } + return &AsyncConn{ + native: n, + handle: handle, + local: local, + remote: &net.TCPAddr{IP: ip, Port: port}, + }, nil +} + +func (n *AsyncNative) ListenContext(ctx context.Context, instance, network, address string) (net.Listener, error) { + if network != "tcp" && network != "tcp4" && network != "tcp6" { + return nil, net.UnknownNetworkError(network) + } + port, err := parseListenPort(address) + if err != nil { + return nil, err + } + handle, local, err := n.TCPBindContext(ctx, instance, uint16(port)) + if err != nil { + return nil, err + } + return &AsyncListener{native: n, handle: handle, addr: local}, nil +} + +func (n *AsyncNative) TCPConnectContext(ctx context.Context, instance, ip string, port uint16) (uint64, *net.TCPAddr, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return 0, nil, err + } + op, err := n.tcpConnectStartCall(instance, ip, port, timeout) + if err != nil { + return 0, nil, err + } + if err := n.waitOp(ctx, op); err != nil { + return 0, nil, err + } + return n.tcpConnectFinishCall(op) +} + +func (n *AsyncNative) TCPBindContext(ctx context.Context, instance string, port uint16) (uint64, *net.TCPAddr, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return 0, nil, err + } + op, err := n.tcpBindStartCall(instance, port, timeout) + if err != nil { + return 0, nil, err + } + if err := n.waitOp(ctx, op); err != nil { + return 0, nil, err + } + return n.tcpBindFinishCall(op) +} + +func (n *AsyncNative) TCPAcceptContext(ctx context.Context, listener uint64) (uint64, *net.TCPAddr, *net.TCPAddr, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return 0, nil, nil, err + } + op, err := n.tcpAcceptStartCall(listener, timeout) + if err != nil { + return 0, nil, nil, err + } + if err := n.waitOp(ctx, op); err != nil { + return 0, nil, nil, err + } + return n.tcpAcceptFinishCall(op) +} + +func (n *AsyncNative) TCPReadContext(ctx context.Context, stream uint64, maxLen uint32) ([]byte, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return nil, err + } + op, err := n.tcpReadStartCall(stream, maxLen, timeout) + if err != nil { + return nil, err + } + if err := n.waitOp(ctx, op); err != nil { + return nil, err + } + return n.tcpReadFinishCall(op) +} + +func (n *AsyncNative) TCPWriteContext(ctx context.Context, stream uint64, data []byte) (int, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return 0, err + } + op, err := n.tcpWriteStartCall(stream, data, timeout) + if err != nil { + return 0, err + } + if err := n.waitOp(ctx, op); err != nil { + return 0, err + } + return n.tcpWriteFinishCall(op) +} + +func (n *AsyncNative) UDPBindContext(ctx context.Context, instance string, port uint16) (*AsyncUDPSocket, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return nil, err + } + op, err := n.udpBindStartCall(instance, port, timeout) + if err != nil { + return nil, err + } + if err := n.waitOp(ctx, op); err != nil { + return nil, err + } + handle, local, err := n.udpBindFinishCall(op) + if err != nil { + return nil, err + } + return &AsyncUDPSocket{native: n, handle: handle, addr: local}, nil +} + +func (n *AsyncNative) UDPSendToContext(ctx context.Context, socket uint64, addr *net.UDPAddr, data []byte) (int, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return 0, err + } + op, err := n.udpSendToStartCall(socket, addr, data, timeout) + if err != nil { + return 0, err + } + if err := n.waitOp(ctx, op); err != nil { + return 0, err + } + return n.udpSendToFinishCall(op) +} + +func (n *AsyncNative) UDPRecvFromContext(ctx context.Context, socket uint64, maxLen uint32) ([]byte, *net.UDPAddr, error) { + timeout, err := contextTimeout(ctx) + if err != nil { + return nil, nil, err + } + op, err := n.udpRecvFromStartCall(socket, maxLen, timeout) + if err != nil { + return nil, nil, err + } + if err := n.waitOp(ctx, op); err != nil { + return nil, nil, err + } + return n.udpRecvFromFinishCall(op) +} + +func (c *AsyncConn) Read(b []byte) (int, error) { + if c.closed.Load() { + return 0, net.ErrClosed + } + if len(b) == 0 { + return 0, nil + } + ctx, cancel := context.WithTimeout(context.Background(), c.rd.timeout(defaultTimeout)) + defer cancel() + data, err := c.native.TCPReadContext(ctx, c.handle, uint32(len(b))) + if err != nil { + return 0, opError("read", c.remote, err) + } + if len(data) == 0 { + return 0, io.EOF + } + return copy(b, data), nil +} + +func (c *AsyncConn) Write(b []byte) (int, error) { + if c.closed.Load() { + return 0, net.ErrClosed + } + ctx, cancel := context.WithTimeout(context.Background(), c.wd.timeout(defaultTimeout)) + defer cancel() + n, err := c.native.TCPWriteContext(ctx, c.handle, b) + if err != nil { + return 0, opError("write", c.remote, err) + } + return n, nil +} + +func (c *AsyncConn) Close() error { + if !c.closed.CompareAndSwap(false, true) { + return net.ErrClosed + } + return c.native.tcpCloseHandle(c.handle) +} + +func (c *AsyncConn) LocalAddr() net.Addr { return c.local } +func (c *AsyncConn) RemoteAddr() net.Addr { return c.remote } +func (c *AsyncConn) SetDeadline(t time.Time) error { c.rd.set(t); c.wd.set(t); return nil } +func (c *AsyncConn) SetReadDeadline(t time.Time) error { c.rd.set(t); return nil } +func (c *AsyncConn) SetWriteDeadline(t time.Time) error { c.wd.set(t); return nil } + +func (l *AsyncListener) Accept() (net.Conn, error) { + if l.closed.Load() { + return nil, net.ErrClosed + } + for { + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + handle, local, peer, err := l.native.TCPAcceptContext(ctx, l.handle) + cancel() + if err == nil { + return &AsyncConn{native: l.native, handle: handle, local: local, remote: peer}, nil + } + if l.closed.Load() { + return nil, net.ErrClosed + } + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + continue + } + return nil, opError("accept", l.addr, err) + } +} + +func (l *AsyncListener) Close() error { + if !l.closed.CompareAndSwap(false, true) { + return net.ErrClosed + } + return l.native.tcpListenerCloseHandle(l.handle) +} + +func (l *AsyncListener) Addr() net.Addr { return l.addr } + +func (s *AsyncUDPSocket) SendTo(ctx context.Context, data []byte, addr *net.UDPAddr) (int, error) { + if s.closed.Load() { + return 0, net.ErrClosed + } + return s.native.UDPSendToContext(ctx, s.handle, addr, data) +} + +func (s *AsyncUDPSocket) RecvFrom(ctx context.Context, maxLen uint32) ([]byte, *net.UDPAddr, error) { + if s.closed.Load() { + return nil, nil, net.ErrClosed + } + return s.native.UDPRecvFromContext(ctx, s.handle, maxLen) +} + +func (s *AsyncUDPSocket) Close() error { + if !s.closed.CompareAndSwap(false, true) { + return net.ErrClosed + } + return s.native.udpCloseHandle(s.handle) +} + +func (s *AsyncUDPSocket) LocalAddr() *net.UDPAddr { return s.addr } + +func (n *AsyncNative) bind() error { + return errors.Join( + n.bindSym(&n.runNetworkInstance, "run_network_instance", types.SInt32TypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.deleteNetworkInst, "delete_network_instance", types.SInt32TypeDescriptor, types.PointerTypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.getErrorMsg, "get_error_msg", types.VoidTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.freeString, "free_string", types.VoidTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.freeBytes, "data_plane_free_bytes", types.VoidTypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor), + n.bindSym(&n.asyncOpStatus, "data_plane_async_op_status", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.asyncOpWait, "data_plane_async_op_wait", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.asyncOpCancel, "data_plane_async_op_cancel", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.asyncOpFree, "data_plane_async_op_free", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpConnectStart, "data_plane_tcp_connect_start", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpConnectFinish, "data_plane_tcp_connect_finish", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.tcpBindStart, "data_plane_tcp_bind_start", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpBindFinish, "data_plane_tcp_bind_finish", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.tcpAcceptStart, "data_plane_tcp_accept_start", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpAcceptFinish, "data_plane_tcp_accept_finish", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.tcpReadStart, "data_plane_tcp_read_start", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpReadFinish, "data_plane_tcp_read_finish", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.tcpWriteStart, "data_plane_tcp_write_start", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpWriteFinish, "data_plane_tcp_write_finish", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpClose, "data_plane_tcp_close", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.tcpListenerClose, "data_plane_tcp_listener_close", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.udpBindStart, "data_plane_udp_bind_start", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.udpBindFinish, "data_plane_udp_bind_finish", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.udpSendToStart, "data_plane_udp_send_to_start", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.udpSendToFinish, "data_plane_udp_send_to_finish", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.udpRecvFromStart, "data_plane_udp_recv_from_start", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor), + n.bindSym(&n.udpRecvFromFinish, "data_plane_udp_recv_from_finish", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor), + n.bindSym(&n.udpClose, "data_plane_udp_close", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor), + ) +} + +func (n *AsyncNative) bindSym(dst *symCall, name string, ret *types.TypeDescriptor, args ...*types.TypeDescriptor) error { + sym, err := ffi.GetSymbol(n.lib, name) + if err != nil { + return err + } + if err := ffi.PrepareCallInterface(&dst.cif, types.DefaultCall, ret, args); err != nil { + return err + } + dst.fn = sym + return nil +} + +func (n *AsyncNative) tcpConnectStartCall(instance, ip string, port uint16, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + inst := cString(instance) + dst := cString(ip) + instPtr := unsafe.Pointer(&inst[0]) + dstPtr := unsafe.Pointer(&dst[0]) + timeoutMS := durationMillis(timeout) + var op uint64 + err := n.tcpConnectStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&instPtr), + unsafe.Pointer(&dstPtr), + unsafe.Pointer(&port), + unsafe.Pointer(&timeoutMS), + ) + runtime.KeepAlive(inst) + runtime.KeepAlive(dst) + return n.startResult(op, err) +} + +func (n *AsyncNative) tcpConnectFinishCall(op uint64) (uint64, *net.TCPAddr, error) { + defer pinErrorThread()() + var handle uint64 + var outIP unsafe.Pointer + outIPArg := unsafe.Pointer(&outIP) + var outPort uint16 + outPortArg := unsafe.Pointer(&outPort) + err := n.tcpConnectFinish.call( + unsafe.Pointer(&handle), + unsafe.Pointer(&op), + unsafe.Pointer(&outIPArg), + unsafe.Pointer(&outPortArg), + ) + if err != nil { + return 0, nil, err + } + if handle == 0 { + return 0, nil, n.lastError() + } + return handle, n.takeTCPAddr(outIP, outPort), nil +} + +func (n *AsyncNative) tcpBindStartCall(instance string, port uint16, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + inst := cString(instance) + instPtr := unsafe.Pointer(&inst[0]) + timeoutMS := durationMillis(timeout) + var op uint64 + err := n.tcpBindStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&instPtr), + unsafe.Pointer(&port), + unsafe.Pointer(&timeoutMS), + ) + runtime.KeepAlive(inst) + return n.startResult(op, err) +} + +func (n *AsyncNative) tcpBindFinishCall(op uint64) (uint64, *net.TCPAddr, error) { + defer pinErrorThread()() + var handle uint64 + var outIP unsafe.Pointer + outIPArg := unsafe.Pointer(&outIP) + var outPort uint16 + outPortArg := unsafe.Pointer(&outPort) + err := n.tcpBindFinish.call( + unsafe.Pointer(&handle), + unsafe.Pointer(&op), + unsafe.Pointer(&outIPArg), + unsafe.Pointer(&outPortArg), + ) + if err != nil { + return 0, nil, err + } + if handle == 0 { + return 0, nil, n.lastError() + } + return handle, n.takeTCPAddr(outIP, outPort), nil +} + +func (n *AsyncNative) tcpAcceptStartCall(listener uint64, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + timeoutMS := durationMillis(timeout) + var op uint64 + err := n.tcpAcceptStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&listener), + unsafe.Pointer(&timeoutMS), + ) + return n.startResult(op, err) +} + +func (n *AsyncNative) tcpAcceptFinishCall(op uint64) (uint64, *net.TCPAddr, *net.TCPAddr, error) { + defer pinErrorThread()() + var handle uint64 + var localIP unsafe.Pointer + localIPArg := unsafe.Pointer(&localIP) + var localPort uint16 + localPortArg := unsafe.Pointer(&localPort) + var peerIP unsafe.Pointer + peerIPArg := unsafe.Pointer(&peerIP) + var peerPort uint16 + peerPortArg := unsafe.Pointer(&peerPort) + err := n.tcpAcceptFinish.call( + unsafe.Pointer(&handle), + unsafe.Pointer(&op), + unsafe.Pointer(&localIPArg), + unsafe.Pointer(&localPortArg), + unsafe.Pointer(&peerIPArg), + unsafe.Pointer(&peerPortArg), + ) + if err != nil { + return 0, nil, nil, err + } + if handle == 0 { + return 0, nil, nil, n.lastError() + } + return handle, n.takeTCPAddr(localIP, localPort), n.takeTCPAddr(peerIP, peerPort), nil +} + +func (n *AsyncNative) tcpReadStartCall(stream uint64, maxLen uint32, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + timeoutMS := durationMillis(timeout) + var op uint64 + err := n.tcpReadStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&stream), + unsafe.Pointer(&maxLen), + unsafe.Pointer(&timeoutMS), + ) + return n.startResult(op, err) +} + +func (n *AsyncNative) tcpReadFinishCall(op uint64) ([]byte, error) { + defer pinErrorThread()() + var ret int32 + var ptr unsafe.Pointer + ptrArg := unsafe.Pointer(&ptr) + var len uint32 + lenArg := unsafe.Pointer(&len) + err := n.tcpReadFinish.call( + unsafe.Pointer(&ret), + unsafe.Pointer(&op), + unsafe.Pointer(&ptrArg), + unsafe.Pointer(&lenArg), + ) + if err != nil { + return nil, err + } + if ret < 0 { + return nil, n.lastError() + } + return n.takeBytes(ptr, len), nil +} + +func (n *AsyncNative) tcpWriteStartCall(stream uint64, data []byte, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + ptr := unsafe.Pointer(nil) + if len(data) > 0 { + ptr = unsafe.Pointer(&data[0]) + } + timeoutMS := durationMillis(timeout) + length := uint32(len(data)) + var op uint64 + err := n.tcpWriteStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&stream), + unsafe.Pointer(&ptr), + unsafe.Pointer(&length), + unsafe.Pointer(&timeoutMS), + ) + runtime.KeepAlive(data) + return n.startResult(op, err) +} + +func (n *AsyncNative) tcpWriteFinishCall(op uint64) (int, error) { + defer pinErrorThread()() + var ret int32 + err := n.tcpWriteFinish.call(unsafe.Pointer(&ret), unsafe.Pointer(&op)) + if err != nil { + return 0, err + } + if ret < 0 { + return 0, n.lastError() + } + return int(ret), nil +} + +func (n *AsyncNative) udpBindStartCall(instance string, port uint16, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + inst := cString(instance) + instPtr := unsafe.Pointer(&inst[0]) + timeoutMS := durationMillis(timeout) + var op uint64 + err := n.udpBindStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&instPtr), + unsafe.Pointer(&port), + unsafe.Pointer(&timeoutMS), + ) + runtime.KeepAlive(inst) + return n.startResult(op, err) +} + +func (n *AsyncNative) udpBindFinishCall(op uint64) (uint64, *net.UDPAddr, error) { + defer pinErrorThread()() + var handle uint64 + var outIP unsafe.Pointer + outIPArg := unsafe.Pointer(&outIP) + var outPort uint16 + outPortArg := unsafe.Pointer(&outPort) + err := n.udpBindFinish.call( + unsafe.Pointer(&handle), + unsafe.Pointer(&op), + unsafe.Pointer(&outIPArg), + unsafe.Pointer(&outPortArg), + ) + if err != nil { + return 0, nil, err + } + if handle == 0 { + return 0, nil, n.lastError() + } + return handle, n.takeUDPAddr(outIP, outPort), nil +} + +func (n *AsyncNative) udpSendToStartCall(socket uint64, addr *net.UDPAddr, data []byte, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + if addr == nil || addr.IP == nil { + return 0, errors.New("udp destination address is nil") + } + dst := cString(addr.IP.String()) + dstPtr := unsafe.Pointer(&dst[0]) + ptr := unsafe.Pointer(nil) + if len(data) > 0 { + ptr = unsafe.Pointer(&data[0]) + } + port := uint16(addr.Port) + length := uint32(len(data)) + timeoutMS := durationMillis(timeout) + var op uint64 + err := n.udpSendToStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&socket), + unsafe.Pointer(&dstPtr), + unsafe.Pointer(&port), + unsafe.Pointer(&ptr), + unsafe.Pointer(&length), + unsafe.Pointer(&timeoutMS), + ) + runtime.KeepAlive(dst) + runtime.KeepAlive(data) + return n.startResult(op, err) +} + +func (n *AsyncNative) udpSendToFinishCall(op uint64) (int, error) { + defer pinErrorThread()() + var ret int32 + err := n.udpSendToFinish.call(unsafe.Pointer(&ret), unsafe.Pointer(&op)) + if err != nil { + return 0, err + } + if ret < 0 { + return 0, n.lastError() + } + return int(ret), nil +} + +func (n *AsyncNative) udpRecvFromStartCall(socket uint64, maxLen uint32, timeout time.Duration) (uint64, error) { + defer pinErrorThread()() + timeoutMS := durationMillis(timeout) + var op uint64 + err := n.udpRecvFromStart.call( + unsafe.Pointer(&op), + unsafe.Pointer(&socket), + unsafe.Pointer(&maxLen), + unsafe.Pointer(&timeoutMS), + ) + return n.startResult(op, err) +} + +func (n *AsyncNative) udpRecvFromFinishCall(op uint64) ([]byte, *net.UDPAddr, error) { + defer pinErrorThread()() + var ret int32 + var ptr unsafe.Pointer + ptrArg := unsafe.Pointer(&ptr) + var len uint32 + lenArg := unsafe.Pointer(&len) + var peerIP unsafe.Pointer + peerIPArg := unsafe.Pointer(&peerIP) + var peerPort uint16 + peerPortArg := unsafe.Pointer(&peerPort) + err := n.udpRecvFromFinish.call( + unsafe.Pointer(&ret), + unsafe.Pointer(&op), + unsafe.Pointer(&ptrArg), + unsafe.Pointer(&lenArg), + unsafe.Pointer(&peerIPArg), + unsafe.Pointer(&peerPortArg), + ) + if err != nil { + return nil, nil, err + } + if ret < 0 { + return nil, nil, n.lastError() + } + return n.takeBytes(ptr, len), n.takeUDPAddr(peerIP, peerPort), nil +} + +func (n *AsyncNative) waitOp(ctx context.Context, op uint64) error { + for { + if err := ctx.Err(); err != nil { + n.cancelAndFreeOp(op) + return err + } + + wait := asyncPollInterval + if deadline, ok := ctx.Deadline(); ok { + remaining := time.Until(deadline) + if remaining <= 0 { + n.cancelAndFreeOp(op) + return context.DeadlineExceeded + } + if remaining < wait { + wait = remaining + } + } + + status, err := n.opWaitStatus(op, wait) + if err != nil { + n.cancelAndFreeOp(op) + return err + } + switch status { + case dataPlaneOpPending: + continue + case dataPlaneOpReady, dataPlaneOpFailed: + return nil + case dataPlaneOpInvalid: + return errors.New("data plane async op is invalid") + default: + return fmt.Errorf("unexpected data plane async op status %d", status) + } + } +} + +func (n *AsyncNative) opWaitStatus(op uint64, timeout time.Duration) (int32, error) { + timeoutMS := durationMillis(timeout) + var status int32 + err := n.asyncOpWait.call( + unsafe.Pointer(&status), + unsafe.Pointer(&op), + unsafe.Pointer(&timeoutMS), + ) + return status, err +} + +func (n *AsyncNative) cancelAndFreeOp(op uint64) { + var ret int32 + _ = n.asyncOpCancel.call(unsafe.Pointer(&ret), unsafe.Pointer(&op)) + _ = n.asyncOpFree.call(unsafe.Pointer(&ret), unsafe.Pointer(&op)) +} + +func (n *AsyncNative) startResult(op uint64, err error) (uint64, error) { + if err != nil { + return 0, err + } + if op == 0 { + return 0, n.lastError() + } + return op, nil +} + +func (n *AsyncNative) lastError() error { + var out unsafe.Pointer + outArg := unsafe.Pointer(&out) + if err := n.getErrorMsg.call(nil, unsafe.Pointer(&outArg)); err != nil { + return err + } + if out == nil { + return errors.New("easytier ffi call failed") + } + msg := readCString(out) + _ = n.freeCString(out) + if msg == "" { + return errors.New("easytier ffi call failed") + } + if containsTimeout(msg) { + return timeoutError(msg) + } + return errors.New(msg) +} + +func (n *AsyncNative) freeCString(ptr unsafe.Pointer) error { + if ptr == nil { + return nil + } + return n.freeString.call(nil, unsafe.Pointer(&ptr)) +} + +func (n *AsyncNative) takeTCPAddr(ipPtr unsafe.Pointer, port uint16) *net.TCPAddr { + if ipPtr == nil { + return nil + } + ip := net.ParseIP(readCString(ipPtr)) + _ = n.freeCString(ipPtr) + return &net.TCPAddr{IP: ip, Port: int(port)} +} + +func (n *AsyncNative) takeUDPAddr(ipPtr unsafe.Pointer, port uint16) *net.UDPAddr { + if ipPtr == nil { + return nil + } + ip := net.ParseIP(readCString(ipPtr)) + _ = n.freeCString(ipPtr) + return &net.UDPAddr{IP: ip, Port: int(port)} +} + +func (n *AsyncNative) takeBytes(ptr unsafe.Pointer, len uint32) []byte { + if ptr == nil || len == 0 { + return nil + } + bytes := make([]byte, int(len)) + copy(bytes, unsafe.Slice((*byte)(ptr), int(len))) + _ = n.freeBytes.call(nil, unsafe.Pointer(&ptr), unsafe.Pointer(&len)) + return bytes +} + +func (n *AsyncNative) tcpCloseHandle(handle uint64) error { + defer pinErrorThread()() + var ret int32 + if err := n.tcpClose.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle)); err != nil { + return err + } + if ret != 0 { + return n.lastError() + } + return nil +} + +func (n *AsyncNative) tcpListenerCloseHandle(handle uint64) error { + defer pinErrorThread()() + var ret int32 + if err := n.tcpListenerClose.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle)); err != nil { + return err + } + if ret != 0 { + return n.lastError() + } + return nil +} + +func (n *AsyncNative) udpCloseHandle(handle uint64) error { + defer pinErrorThread()() + var ret int32 + if err := n.udpClose.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle)); err != nil { + return err + } + if ret != 0 { + return n.lastError() + } + return nil +} + +func contextTimeout(ctx context.Context) (time.Duration, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + timeout := defaultTimeout + if deadline, ok := ctx.Deadline(); ok { + timeout = time.Until(deadline) + } + if timeout <= 0 { + return 0, context.DeadlineExceeded + } + return timeout, nil +} + +func durationMillis(d time.Duration) uint64 { + if d <= 0 { + return 0 + } + ms := d / time.Millisecond + if ms <= 0 { + return 1 + } + return uint64(ms) +} + +func containsTimeout(msg string) bool { + return strings.Contains(msg, "timed out") || strings.Contains(msg, "timeout") +} + +func (b *atomicBool) Load() bool { + return b.v.Load() +} + +func (b *atomicBool) CompareAndSwap(old, new bool) bool { + return b.v.CompareAndSwap(old, new) +} + +var _ net.Conn = (*AsyncConn)(nil) +var _ net.Listener = (*AsyncListener)(nil) diff --git a/easytier-contrib/easytier-ffi/examples/go/easytier_async_test.go b/easytier-contrib/easytier-ffi/examples/go/easytier_async_test.go new file mode 100644 index 00000000..195b1dc0 --- /dev/null +++ b/easytier-contrib/easytier-ffi/examples/go/easytier_async_test.go @@ -0,0 +1,360 @@ +package easytierffi + +import ( + "context" + "fmt" + "io" + "net" + "os" + "strconv" + "testing" + "time" +) + +const asyncLocalTestTimeout = 120 * time.Second + +func TestAsyncSymbolBinding(t *testing.T) { + n := openAsyncForTest(t) + + status, err := n.opWaitStatus(0, 0) + if err != nil { + t.Fatal(err) + } + if status != dataPlaneOpInvalid { + t.Fatalf("expected invalid status for op 0, got %d", status) + } +} + +func TestAsyncLocalTwoNodeTCPAndUDP(t *testing.T) { + n := openAsyncForTest(t) + topology := startLocalAsyncTopology(t, n) + + ctx, cancel := context.WithTimeout(context.Background(), asyncLocalTestTimeout) + defer cancel() + + runAsyncTCPPingPong(t, ctx, n, topology) + runAsyncUDPPingPong(t, ctx, n, topology) +} + +type localAsyncTopology struct { + dialerInstance string + listenerInstance string + listenerIP string +} + +func openAsyncForTest(t *testing.T) *AsyncNative { + t.Helper() + + libraryPath := defaultLibraryPath() + if _, err := os.Stat(libraryPath); err != nil { + if os.IsNotExist(err) { + t.Skipf("build easytier-ffi with ffi-dataplane before running async tests: %v", err) + } + t.Fatalf("stat async ffi library: %v", err) + } + + n, err := OpenAsync(libraryPath) + if err != nil { + t.Fatalf("open async ffi library: %v", err) + } + t.Cleanup(func() { + if err := n.Close(); err != nil { + t.Errorf("close async native: %v", err) + } + }) + return n +} + +func startLocalAsyncTopology(t *testing.T, n *AsyncNative) localAsyncTopology { + t.Helper() + + suffix := strconv.FormatInt(time.Now().UnixNano(), 10) + networkName := "ffi-async-" + suffix + networkSecret := "ffi-async-secret-" + suffix + listenerInstance := "ffi-async-listener-" + suffix + dialerInstance := "ffi-async-dialer-" + suffix + listenerIP := "10.251.1.2" + dialerIP := "10.251.1.1" + listenerPort := freeLocalTCPPort(t) + listenerEndpoint := fmt.Sprintf("tcp://127.0.0.1:%d", listenerPort) + t.Cleanup(func() { + if err := n.deleteNetworkInstances([]string{dialerInstance, listenerInstance}); err != nil { + t.Errorf("cleanup async test EasyTier instances: %v", err) + } + }) + + listenerConfig := localAsyncConfig( + listenerInstance, + listenerIP, + networkName, + networkSecret, + []string{listenerEndpoint}, + nil, + ) + dialerConfig := localAsyncConfig( + dialerInstance, + dialerIP, + networkName, + networkSecret, + nil, + []string{listenerEndpoint}, + ) + + if err := n.RunNetworkInstance(listenerConfig); err != nil { + t.Fatalf("start listener instance: %v", err) + } + if err := n.RunNetworkInstance(dialerConfig); err != nil { + t.Fatalf("start dialer instance: %v", err) + } + + return localAsyncTopology{ + dialerInstance: dialerInstance, + listenerInstance: listenerInstance, + listenerIP: listenerIP, + } +} + +func localAsyncConfig(instance, ipv4, networkName, networkSecret string, listeners, peers []string) string { + config := fmt.Sprintf(`instance_name = %s +ipv4 = %s +listeners = %s + +[network_identity] +network_name = %s +network_secret = %s + +[flags] +no_tun = true +bind_device = false +`, + strconv.Quote(instance), + strconv.Quote(ipv4), + tomlStringList(listeners), + strconv.Quote(networkName), + strconv.Quote(networkSecret), + ) + for _, peer := range peers { + config += fmt.Sprintf("\n[[peer]]\nuri = %s\n", strconv.Quote(peer)) + } + return config +} + +func tomlStringList(values []string) string { + if len(values) == 0 { + return "[]" + } + + out := "[" + for i, value := range values { + if i > 0 { + out += ", " + } + out += strconv.Quote(value) + } + return out + "]" +} + +func freeLocalTCPPort(t *testing.T) int { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("allocate local tcp port: %v", err) + } + defer listener.Close() + return listener.Addr().(*net.TCPAddr).Port +} + +func runAsyncTCPPingPong(t *testing.T, ctx context.Context, n *AsyncNative, topology localAsyncTopology) { + t.Helper() + + listener, listenerAddr := eventuallyTCPListen(t, ctx, n, topology.listenerInstance) + + tcpCtx, cancel := context.WithCancel(ctx) + accepted := make(chan error, 1) + defer waitForAsyncHelper(t, accepted, "tcp accept helper") + defer cancel() + defer listener.Close() + go func() { + conn, err := listener.Accept() + if err != nil { + accepted <- fmt.Errorf("accept tcp stream: %w", err) + return + } + defer conn.Close() + _ = conn.SetDeadline(time.Now().Add(30 * time.Second)) + + payload := make([]byte, len("ping")) + if _, err := io.ReadFull(conn, payload); err != nil { + accepted <- fmt.Errorf("read tcp ping: %w", err) + return + } + if string(payload) != "ping" { + accepted <- fmt.Errorf("expected tcp ping, got %q", string(payload)) + return + } + if _, err := conn.Write([]byte("pong")); err != nil { + accepted <- fmt.Errorf("write tcp pong: %w", err) + return + } + accepted <- nil + }() + + conn, err := eventuallyTCPDial(t, tcpCtx, n, topology.dialerInstance, topology.listenerIP, listenerAddr.Port) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + _ = conn.SetDeadline(time.Now().Add(30 * time.Second)) + + if _, err := conn.Write([]byte("ping")); err != nil { + t.Fatalf("write tcp ping: %v", err) + } + payload := make([]byte, len("pong")) + if _, err := io.ReadFull(conn, payload); err != nil { + t.Fatalf("read tcp pong: %v", err) + } + if string(payload) != "pong" { + t.Fatalf("expected tcp pong, got %q", string(payload)) + } +} + +func eventuallyTCPListen(t *testing.T, ctx context.Context, n *AsyncNative, instance string) (net.Listener, *net.TCPAddr) { + t.Helper() + + var lastErr error + for attempt := 1; ctx.Err() == nil; attempt++ { + attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + listener, err := n.ListenContext(attemptCtx, instance, "tcp", "0.0.0.0:0") + cancel() + if err == nil { + addr := listener.Addr().(*net.TCPAddr) + t.Logf("async tcp bind succeeded on attempt %d at %s", attempt, addr) + return listener, addr + } + + lastErr = err + t.Logf("attempt %d: async tcp bind failed: %v", attempt, err) + waitForRetry(ctx, 500*time.Millisecond) + } + t.Fatalf("async tcp bind never succeeded: %v", lastErr) + panic("unreachable") +} + +func eventuallyTCPDial(t *testing.T, ctx context.Context, n *AsyncNative, instance, ip string, port int) (net.Conn, error) { + t.Helper() + + address := net.JoinHostPort(ip, strconv.Itoa(port)) + var lastErr error + for attempt := 1; ctx.Err() == nil; attempt++ { + attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + conn, err := n.DialContext(attemptCtx, instance, "tcp", address) + cancel() + if err == nil { + t.Logf("async tcp connect succeeded on attempt %d to %s", attempt, address) + return conn, nil + } + + lastErr = err + t.Logf("attempt %d: async tcp connect failed: %v", attempt, err) + waitForRetry(ctx, 500*time.Millisecond) + } + return nil, fmt.Errorf("async tcp connect never succeeded: %w", lastErr) +} + +func runAsyncUDPPingPong(t *testing.T, ctx context.Context, n *AsyncNative, topology localAsyncTopology) { + t.Helper() + + dialerSocket, err := n.UDPBindContext(ctx, topology.dialerInstance, 0) + if err != nil { + t.Fatalf("bind dialer udp socket: %v", err) + } + + listenerSocket, err := n.UDPBindContext(ctx, topology.listenerInstance, 0) + if err != nil { + t.Fatalf("bind listener udp socket: %v", err) + } + + udpCtx, cancel := context.WithCancel(ctx) + warmupDone := make(chan error, 1) + received := make(chan error, 1) + defer waitForAsyncHelper(t, received, "udp receive helper") + defer cancel() + defer listenerSocket.Close() + defer dialerSocket.Close() + + go func() { + if _, err := listenerSocket.SendTo(udpCtx, []byte("warmup"), dialerSocket.LocalAddr()); err != nil { + err = fmt.Errorf("send udp warmup: %w", err) + warmupDone <- err + received <- err + return + } + warmupDone <- nil + + payload, from, err := listenerSocket.RecvFrom(udpCtx, 512) + if err != nil { + received <- fmt.Errorf("recv udp ping: %w", err) + return + } + if string(payload) != "ping" { + received <- fmt.Errorf("expected udp ping, got %q", string(payload)) + return + } + if _, err := listenerSocket.SendTo(udpCtx, []byte("pong"), from); err != nil { + received <- fmt.Errorf("send udp pong: %w", err) + return + } + received <- nil + }() + + select { + case err := <-warmupDone: + if err != nil { + t.Fatal(err) + } + case <-udpCtx.Done(): + t.Fatal(udpCtx.Err()) + } + + target := &net.UDPAddr{IP: net.ParseIP(topology.listenerIP), Port: listenerSocket.LocalAddr().Port} + if _, err := dialerSocket.SendTo(udpCtx, []byte("ping"), target); err != nil { + t.Fatalf("send udp ping: %v", err) + } + for { + payload, from, err := dialerSocket.RecvFrom(udpCtx, 512) + if err != nil { + t.Fatalf("recv udp pong: %v", err) + } + if string(payload) == "pong" { + if !from.IP.Equal(target.IP) || from.Port != target.Port { + t.Fatalf("expected udp pong from %s, got %s", target, from) + } + break + } + t.Logf("skipping udp datagram from %s: %q", from, string(payload)) + } +} + +func waitForAsyncHelper(t *testing.T, done <-chan error, name string) { + t.Helper() + + select { + case err := <-done: + if err != nil { + t.Errorf("%s: %v", name, err) + } + case <-time.After(10 * time.Second): + t.Errorf("%s did not stop", name) + } +} + +func waitForRetry(ctx context.Context, delay time.Duration) { + timer := time.NewTimer(delay) + defer timer.Stop() + + select { + case <-timer.C: + case <-ctx.Done(): + } +} diff --git a/easytier-contrib/easytier-ffi/examples/go/go.sum b/easytier-contrib/easytier-ffi/examples/go/go.sum new file mode 100644 index 00000000..4161cdc2 --- /dev/null +++ b/easytier-contrib/easytier-ffi/examples/go/go.sum @@ -0,0 +1,2 @@ +github.com/go-webgpu/goffi v0.4.1 h1:2hQH5XXloxTyTtIleYv+Rajlwzp6UOETURhSZ5+zJxU= +github.com/go-webgpu/goffi v0.4.1/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM= diff --git a/easytier-contrib/easytier-ffi/src/config_server.rs b/easytier-contrib/easytier-ffi/src/config_server.rs index 185d82a2..511e6939 100644 --- a/easytier-contrib/easytier-ffi/src/config_server.rs +++ b/easytier-contrib/easytier-ffi/src/config_server.rs @@ -465,7 +465,7 @@ pub(crate) unsafe fn start_config_server_client( clear_last_callback_error(); #[cfg(feature = "ffi-dataplane")] - let _data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() { + let data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() { Ok(guard) => guard, Err(err) => { set_error_msg(&err); @@ -474,6 +474,9 @@ pub(crate) unsafe fn start_config_server_client( }; CONFIG_SERVER_CLIENT_ACTIVE.store(true, Ordering::Release); + #[cfg(feature = "ffi-dataplane")] + drop(data_plane_usage_guard); + let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data)); let client = match ASYNC_RUNTIME.block_on(run_web_client( &config_server_url, diff --git a/easytier-contrib/easytier-ffi/src/data_plane.rs b/easytier-contrib/easytier-ffi/src/data_plane.rs index d96c7c9e..a565c85c 100644 --- a/easytier-contrib/easytier-ffi/src/data_plane.rs +++ b/easytier-contrib/easytier-ffi/src/data_plane.rs @@ -37,22 +37,22 @@ static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| RwLock::new(())); #[cfg(feature = "ffi-dataplane")] -struct DataPlaneHandle { - instance_id: uuid::Uuid, - runtime: tokio::runtime::Handle, +pub(crate) struct DataPlaneHandle { + pub(crate) instance_id: uuid::Uuid, + pub(crate) runtime: tokio::runtime::Handle, // Cancelled by close() to wake any in-flight op on this handle. - close_token: CancellationToken, - resource: DataPlaneResource, + pub(crate) close_token: CancellationToken, + pub(crate) resource: DataPlaneResource, } #[cfg(feature = "ffi-dataplane")] -struct TcpHalves { - read: tokio::sync::Mutex>, - write: tokio::sync::Mutex>, +pub(crate) struct TcpHalves { + pub(crate) read: tokio::sync::Mutex>, + pub(crate) write: tokio::sync::Mutex>, } #[cfg(feature = "ffi-dataplane")] -enum DataPlaneResource { +pub(crate) enum DataPlaneResource { Tcp(Arc), TcpListener(Arc>), Udp(Arc), @@ -61,17 +61,17 @@ enum DataPlaneResource { // Several helper functions for FFI data plane operations to facilitate logic reuse. #[cfg(feature = "ffi-dataplane")] -fn next_handle() -> u64 { +pub(crate) fn next_handle() -> u64 { NEXT_DATA_PLANE_HANDLE.fetch_add(1, Ordering::Relaxed) } #[cfg(feature = "ffi-dataplane")] -fn timeout_duration(timeout_ms: u64) -> Duration { +pub(crate) fn timeout_duration(timeout_ms: u64) -> Duration { Duration::from_millis(timeout_ms) } #[cfg(feature = "ffi-dataplane")] -unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option { +pub(crate) unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option { if ptr.is_null() { set_error_msg(&format!("{} is null", name)); return None; @@ -84,12 +84,12 @@ unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option Option { +pub(crate) fn get_instance_id(inst_name: &str) -> Option { INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id.value()) } #[cfg(feature = "ffi-dataplane")] -fn parse_socket_addr(host: &str, port: u16) -> Option { +pub(crate) fn parse_socket_addr(host: &str, port: u16) -> Option { let ip = match host.parse::() { Ok(ip) => ip, Err(e) => { @@ -103,7 +103,7 @@ fn parse_socket_addr(host: &str, port: u16) -> Option { /// Encode an IP address for FFI return. Returns `*mut c_char` to match /// `CString::into_raw`; caller releases it via `free_string`. #[cfg(feature = "ffi-dataplane")] -fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> { +pub(crate) fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> { match std::ffi::CString::new(ip.to_string()) { Ok(s) => Some(s.into_raw()), Err(e) => { @@ -114,7 +114,7 @@ fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> { } #[cfg(feature = "ffi-dataplane")] -fn get_runtime_handle( +pub(crate) fn get_runtime_handle( inst_id: &uuid::Uuid, deadline: std::time::Instant, ) -> Option { @@ -127,7 +127,7 @@ fn get_runtime_handle( } #[cfg(feature = "ffi-dataplane")] -fn insert_tcp_stream_handle( +pub(crate) fn insert_tcp_stream_handle( instance_id: uuid::Uuid, runtime: tokio::runtime::Handle, stream: DataPlaneTcpStream, @@ -150,17 +150,71 @@ fn insert_tcp_stream_handle( } #[cfg(feature = "ffi-dataplane")] -fn get_tcp_stream( +pub(crate) fn insert_tcp_listener_handle( + instance_id: uuid::Uuid, + runtime: tokio::runtime::Handle, + listener: DataPlaneTcpListener, +) -> u64 { + let handle = next_handle(); + DATA_PLANE_HANDLES.insert( + handle, + DataPlaneHandle { + instance_id, + runtime, + close_token: CancellationToken::new(), + resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new(listener))), + }, + ); + handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn insert_udp_socket_handle( + instance_id: uuid::Uuid, + runtime: tokio::runtime::Handle, + socket: DataPlaneUdpSocket, +) -> u64 { + let handle = next_handle(); + DATA_PLANE_HANDLES.insert( + handle, + DataPlaneHandle { + instance_id, + runtime, + close_token: CancellationToken::new(), + resource: DataPlaneResource::Udp(Arc::new(socket)), + }, + ); + handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn get_tcp_stream( handle: u64, ) -> Option<(Arc, tokio::runtime::Handle, CancellationToken)> { + get_tcp_stream_with_instance(handle) + .map(|(halves, runtime, close_token, _)| (halves, runtime, close_token)) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn get_tcp_stream_with_instance( + handle: u64, +) -> Option<( + Arc, + tokio::runtime::Handle, + CancellationToken, + uuid::Uuid, +)> { let Some(h) = DATA_PLANE_HANDLES.get(&handle) else { set_error_msg("tcp stream handle not found"); return None; }; match &h.resource { - DataPlaneResource::Tcp(halves) => { - Some((halves.clone(), h.runtime.clone(), h.close_token.clone())) - } + DataPlaneResource::Tcp(halves) => Some(( + halves.clone(), + h.runtime.clone(), + h.close_token.clone(), + h.instance_id, + )), DataPlaneResource::TcpListener(_) | DataPlaneResource::Udp(_) => { set_error_msg("handle is not a tcp stream"); None @@ -169,7 +223,7 @@ fn get_tcp_stream( } #[cfg(feature = "ffi-dataplane")] -fn get_tcp_listener( +pub(crate) fn get_tcp_listener( handle: u64, ) -> Option<( Arc>, @@ -196,21 +250,37 @@ fn get_tcp_listener( } #[cfg(feature = "ffi-dataplane")] -fn get_udp_socket( +pub(crate) fn get_udp_socket( handle: u64, ) -> Option<( Arc, tokio::runtime::Handle, CancellationToken, +)> { + get_udp_socket_with_instance(handle) + .map(|(socket, runtime, close_token, _)| (socket, runtime, close_token)) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn get_udp_socket_with_instance( + handle: u64, +) -> Option<( + Arc, + tokio::runtime::Handle, + CancellationToken, + uuid::Uuid, )> { let Some(h) = DATA_PLANE_HANDLES.get(&handle) else { set_error_msg("udp socket handle not found"); return None; }; match &h.resource { - DataPlaneResource::Udp(socket) => { - Some((socket.clone(), h.runtime.clone(), h.close_token.clone())) - } + DataPlaneResource::Udp(socket) => Some(( + socket.clone(), + h.runtime.clone(), + h.close_token.clone(), + h.instance_id, + )), DataPlaneResource::Tcp(_) | DataPlaneResource::TcpListener(_) => { set_error_msg("handle is not a udp socket"); None @@ -224,6 +294,10 @@ pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) { return; } + let _data_plane_usage_guard = DATA_PLANE_USAGE_LOCK + .write() + .unwrap_or_else(|err| err.into_inner()); + DATA_PLANE_HANDLES.retain(|_, handle| { if ids.contains(&handle.instance_id) { handle.close_token.cancel(); @@ -232,13 +306,14 @@ pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) { true } }); + crate::data_plane_async::remove_ops_by_instance_ids(ids); } #[cfg(not(feature = "ffi-dataplane"))] pub(crate) fn remove_data_plane_handles_by_instance_ids(_ids: &[uuid::Uuid]) {} #[cfg(feature = "ffi-dataplane")] -fn data_plane_rejected() -> bool { +pub(crate) fn data_plane_rejected() -> bool { if in_config_server_callback() { set_error_msg("cannot use data plane from config server callback"); true @@ -251,7 +326,7 @@ fn data_plane_rejected() -> bool { } #[cfg(feature = "ffi-dataplane")] -fn enter_data_plane_operation() -> Option> { +pub(crate) fn enter_data_plane_operation() -> Option> { if data_plane_rejected() { return None; } @@ -303,7 +378,7 @@ pub(crate) fn lock_for_config_server_start() let guard = DATA_PLANE_USAGE_LOCK .write() .map_err(|err| format!("failed to lock data plane usage: {}", err))?; - if !DATA_PLANE_HANDLES.is_empty() { + if !DATA_PLANE_HANDLES.is_empty() || crate::data_plane_async::has_live_ops() { return Err("cannot start config server client while data plane is in use".to_string()); } Ok(guard) @@ -413,18 +488,7 @@ pub(crate) unsafe fn data_plane_tcp_bind( let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else { return 0; }; - let handle = next_handle(); - DATA_PLANE_HANDLES.insert( - handle, - DataPlaneHandle { - instance_id: inst_id, - runtime, - close_token: CancellationToken::new(), - resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new( - listener, - ))), - }, - ); + let handle = insert_tcp_listener_handle(inst_id, runtime, listener); unsafe { *out_local_ip = local_ip as *const std::ffi::c_char; *out_local_port = local_addr.port(); @@ -600,6 +664,7 @@ pub(crate) fn data_plane_tcp_close(handle: u64) -> std::ffi::c_int { Some(guard) => guard, None => return -1, }; + crate::data_plane_async::cancel_ops_for_handle(handle); let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| { matches!(e.resource, DataPlaneResource::Tcp(_)) }) else { @@ -629,6 +694,7 @@ pub(crate) fn data_plane_tcp_listener_close(handle: u64) -> std::ffi::c_int { Some(guard) => guard, None => return -1, }; + crate::data_plane_async::cancel_ops_for_handle(handle); let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| { matches!(e.resource, DataPlaneResource::TcpListener(_)) }) else { @@ -685,16 +751,7 @@ pub(crate) unsafe fn data_plane_udp_bind( let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else { return 0; }; - let handle = next_handle(); - DATA_PLANE_HANDLES.insert( - handle, - DataPlaneHandle { - instance_id: inst_id, - runtime, - close_token: CancellationToken::new(), - resource: DataPlaneResource::Udp(Arc::new(socket)), - }, - ); + let handle = insert_udp_socket_handle(inst_id, runtime, socket); unsafe { *out_local_ip = local_ip as *const std::ffi::c_char; *out_local_port = local_addr.port(); @@ -818,6 +875,7 @@ pub(crate) fn data_plane_udp_close(handle: u64) -> std::ffi::c_int { Some(guard) => guard, None => return -1, }; + crate::data_plane_async::cancel_ops_for_handle(handle); let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| { matches!(e.resource, DataPlaneResource::Udp(_)) }) else { @@ -851,4 +909,20 @@ mod tests { done_rx.recv_timeout(Duration::from_secs(5)).unwrap(); waiter.join().unwrap(); } + + #[test] + fn instance_cleanup_waits_for_data_plane_operation() { + let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap(); + let instance_id = Uuid::new_v4(); + let (done_tx, done_rx) = mpsc::channel(); + let cleaner = std::thread::spawn(move || { + remove_data_plane_handles_by_instance_ids(&[instance_id]); + done_tx.send(()).unwrap(); + }); + + assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err()); + drop(read_guard); + done_rx.recv_timeout(Duration::from_secs(5)).unwrap(); + cleaner.join().unwrap(); + } } diff --git a/easytier-contrib/easytier-ffi/src/data_plane_async.rs b/easytier-contrib/easytier-ffi/src/data_plane_async.rs new file mode 100644 index 00000000..8fb684a3 --- /dev/null +++ b/easytier-contrib/easytier-ffi/src/data_plane_async.rs @@ -0,0 +1,1162 @@ +#[cfg(feature = "ffi-dataplane")] +use std::{ + future::Future, + net::SocketAddr, + sync::{ + Arc, Condvar, Mutex, + atomic::{AtomicU64, Ordering}, + }, + time::{Duration, Instant}, +}; + +#[cfg(feature = "ffi-dataplane")] +use dashmap::DashMap; +#[cfg(feature = "ffi-dataplane")] +use easytier::launcher::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket}; +#[cfg(feature = "ffi-dataplane")] +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[cfg(feature = "ffi-dataplane")] +use tokio_util::sync::CancellationToken; +#[cfg(feature = "ffi-dataplane")] +use uuid::Uuid; + +#[cfg(feature = "ffi-dataplane")] +use crate::{ + data_plane::{ + TcpHalves, cstr_to_string, enter_data_plane_operation, get_instance_id, get_tcp_listener, + get_tcp_stream_with_instance, get_udp_socket_with_instance, insert_tcp_listener_handle, + insert_tcp_stream_handle, insert_udp_socket_handle, into_ffi_ip_cstring, parse_socket_addr, + timeout_duration, + }, + error::{free_string, set_error_msg}, + state::{ASYNC_RUNTIME, INSTANCE_MANAGER}, +}; + +#[cfg(feature = "ffi-dataplane")] +pub(crate) const DATA_PLANE_OP_PENDING: std::ffi::c_int = 0; +#[cfg(feature = "ffi-dataplane")] +pub(crate) const DATA_PLANE_OP_READY: std::ffi::c_int = 1; +#[cfg(feature = "ffi-dataplane")] +pub(crate) const DATA_PLANE_OP_FAILED: std::ffi::c_int = -1; +#[cfg(feature = "ffi-dataplane")] +pub(crate) const DATA_PLANE_OP_INVALID: std::ffi::c_int = -2; + +#[cfg(feature = "ffi-dataplane")] +static NEXT_DATA_PLANE_OP: AtomicU64 = AtomicU64::new(1); +#[cfg(feature = "ffi-dataplane")] +static DATA_PLANE_OPS: once_cell::sync::Lazy>> = + once_cell::sync::Lazy::new(DashMap::new); + +#[cfg(feature = "ffi-dataplane")] +const MAX_ASYNC_READ_LEN: u32 = 16 * 1024 * 1024; +#[cfg(feature = "ffi-dataplane")] +const MAX_ASYNC_WRITE_LEN: u32 = std::ffi::c_int::MAX as u32; + +#[cfg(feature = "ffi-dataplane")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DataPlaneAsyncOpKind { + TcpConnect, + TcpBind, + TcpAccept, + TcpRead, + TcpWrite, + UdpBind, + UdpSendTo, + UdpRecvFrom, +} + +#[cfg(feature = "ffi-dataplane")] +struct DataPlaneAsyncOp { + kind: DataPlaneAsyncOpKind, + instance_id: Option, + target_handle: Option, + cancel_token: CancellationToken, + state: Mutex, + ready: Condvar, +} + +#[cfg(feature = "ffi-dataplane")] +enum DataPlaneAsyncOpState { + Pending, + Ready(DataPlaneAsyncOpResult), + Failed(String), + Consumed, +} + +#[cfg(feature = "ffi-dataplane")] +enum DataPlaneAsyncOpResult { + TcpConnect { + instance_id: Uuid, + runtime: tokio::runtime::Handle, + stream: DataPlaneTcpStream, + local_addr: SocketAddr, + }, + TcpBind { + instance_id: Uuid, + runtime: tokio::runtime::Handle, + listener: DataPlaneTcpListener, + local_addr: SocketAddr, + }, + TcpAccept { + instance_id: Uuid, + runtime: tokio::runtime::Handle, + stream: DataPlaneTcpStream, + local_addr: SocketAddr, + peer_addr: SocketAddr, + }, + TcpRead { + data: Vec, + }, + TcpWrite { + written: usize, + }, + UdpBind { + instance_id: Uuid, + runtime: tokio::runtime::Handle, + socket: DataPlaneUdpSocket, + local_addr: SocketAddr, + }, + UdpSendTo { + sent: usize, + }, + UdpRecvFrom { + data: Vec, + peer_addr: SocketAddr, + }, +} + +#[cfg(feature = "ffi-dataplane")] +fn next_op_handle() -> u64 { + NEXT_DATA_PLANE_OP.fetch_add(1, Ordering::Relaxed) +} + +#[cfg(feature = "ffi-dataplane")] +fn new_op( + kind: DataPlaneAsyncOpKind, + instance_id: Option, + target_handle: Option, +) -> (u64, Arc) { + let handle = next_op_handle(); + let op = Arc::new(DataPlaneAsyncOp { + kind, + instance_id, + target_handle, + cancel_token: CancellationToken::new(), + state: Mutex::new(DataPlaneAsyncOpState::Pending), + ready: Condvar::new(), + }); + DATA_PLANE_OPS.insert(handle, op.clone()); + (handle, op) +} + +#[cfg(feature = "ffi-dataplane")] +fn complete_op(op: &DataPlaneAsyncOp, result: Result) { + let Ok(mut state) = op.state.lock() else { + return; + }; + if !matches!(*state, DataPlaneAsyncOpState::Pending) { + return; + } + *state = match result { + Ok(result) => DataPlaneAsyncOpState::Ready(result), + Err(err) => DataPlaneAsyncOpState::Failed(err), + }; + op.ready.notify_all(); +} + +#[cfg(feature = "ffi-dataplane")] +fn cancel_pending_op(op: &DataPlaneAsyncOp, reason: &str) { + op.cancel_token.cancel(); + let Ok(mut state) = op.state.lock() else { + return; + }; + if matches!(*state, DataPlaneAsyncOpState::Pending) { + *state = DataPlaneAsyncOpState::Failed(reason.to_string()); + op.ready.notify_all(); + } +} + +#[cfg(feature = "ffi-dataplane")] +fn validate_max_len(max_len: u32) -> bool { + if max_len > MAX_ASYNC_READ_LEN { + set_error_msg(&format!( + "max_len exceeds async data plane limit of {} bytes", + MAX_ASYNC_READ_LEN + )); + false + } else { + true + } +} + +#[cfg(feature = "ffi-dataplane")] +fn validate_write_len(len: u32) -> bool { + if len > MAX_ASYNC_WRITE_LEN { + set_error_msg(&format!( + "len exceeds async data plane write limit of {} bytes", + MAX_ASYNC_WRITE_LEN + )); + false + } else { + true + } +} + +#[cfg(feature = "ffi-dataplane")] +fn usize_to_c_int(value: usize, name: &str) -> Option { + if value > std::ffi::c_int::MAX as usize { + set_error_msg(&format!( + "{} exceeds c_int limit of {}", + name, + std::ffi::c_int::MAX + )); + None + } else { + Some(value as std::ffi::c_int) + } +} + +#[cfg(feature = "ffi-dataplane")] +fn spawn_instance_runtime_op( + op: Arc, + instance_id: Uuid, + timeout_ms: u64, + build: F, +) where + Fut: Future> + Send + 'static, + F: FnOnce(tokio::runtime::Handle, Duration) -> Fut + Send + 'static, +{ + let deadline = Instant::now() + timeout_duration(timeout_ms); + ASYNC_RUNTIME.spawn_blocking(move || { + let runtime = loop { + if op.cancel_token.is_cancelled() { + complete_op(&op, Err("data plane async op canceled".to_string())); + return; + } + + let remaining = deadline.saturating_duration_since(Instant::now()); + let wait_for = remaining.min(Duration::from_millis(50)); + if let Some(runtime) = + INSTANCE_MANAGER.data_plane_wait_runtime_handle(&instance_id, wait_for) + { + break runtime; + } + if remaining.is_zero() || Instant::now() >= deadline { + complete_op(&op, Err("instance runtime is not ready".to_string())); + return; + } + }; + if op.cancel_token.is_cancelled() { + complete_op(&op, Err("data plane async op canceled".to_string())); + return; + } + + let runtime_for_task = runtime.clone(); + let op_for_complete = op.clone(); + runtime.spawn(async move { + let remaining = deadline.saturating_duration_since(Instant::now()); + let result = build(runtime_for_task, remaining).await; + complete_op(&op_for_complete, result); + }); + }); +} + +#[cfg(feature = "ffi-dataplane")] +fn state_status(state: &DataPlaneAsyncOpState) -> std::ffi::c_int { + match state { + DataPlaneAsyncOpState::Pending => DATA_PLANE_OP_PENDING, + DataPlaneAsyncOpState::Ready(_) => DATA_PLANE_OP_READY, + DataPlaneAsyncOpState::Failed(_) => DATA_PLANE_OP_FAILED, + DataPlaneAsyncOpState::Consumed => DATA_PLANE_OP_INVALID, + } +} + +#[cfg(feature = "ffi-dataplane")] +fn take_completed_op( + handle: u64, + expected: DataPlaneAsyncOpKind, +) -> Option { + let Some((_, op)) = DATA_PLANE_OPS.remove_if(&handle, |_, op| { + if op.kind != expected { + return false; + } + let Ok(state) = op.state.lock() else { + return false; + }; + match &*state { + DataPlaneAsyncOpState::Ready(_) | DataPlaneAsyncOpState::Failed(_) => true, + DataPlaneAsyncOpState::Pending | DataPlaneAsyncOpState::Consumed => false, + } + }) else { + let Some(op) = DATA_PLANE_OPS.get(&handle).map(|op| op.clone()) else { + set_error_msg("data plane async op not found"); + return None; + }; + if op.kind != expected { + set_error_msg("data plane async op type mismatch"); + return None; + } + let Ok(state) = op.state.lock() else { + set_error_msg("failed to lock data plane async op"); + return None; + }; + match &*state { + DataPlaneAsyncOpState::Pending => { + set_error_msg("data plane async op is still pending"); + return None; + } + DataPlaneAsyncOpState::Consumed => { + set_error_msg("data plane async op already consumed"); + return None; + } + DataPlaneAsyncOpState::Ready(_) | DataPlaneAsyncOpState::Failed(_) => { + set_error_msg("data plane async op was consumed concurrently"); + } + } + return None; + }; + + let completed = { + let Ok(mut state) = op.state.lock() else { + set_error_msg("failed to lock data plane async op"); + return None; + }; + std::mem::replace(&mut *state, DataPlaneAsyncOpState::Consumed) + }; + + match completed { + DataPlaneAsyncOpState::Ready(result) => Some(result), + DataPlaneAsyncOpState::Failed(err) => { + set_error_msg(&err); + None + } + DataPlaneAsyncOpState::Pending | DataPlaneAsyncOpState::Consumed => None, + } +} + +#[cfg(feature = "ffi-dataplane")] +async fn run_with_cancel( + cancel_token: &CancellationToken, + error_prefix: &str, + op: F, +) -> Result +where + E: std::fmt::Display, + F: Future>, +{ + tokio::select! { + biased; + _ = cancel_token.cancelled() => Err(format!("{}: operation canceled", error_prefix)), + res = op => res.map_err(|err| format!("{}: {}", error_prefix, err)), + } +} + +#[cfg(feature = "ffi-dataplane")] +async fn run_io_with_cancel( + cancel_token: &CancellationToken, + close_token: &CancellationToken, + timeout_ms: u64, + error_prefix: &str, + op: F, +) -> Result +where + F: Future>, +{ + tokio::select! { + biased; + _ = cancel_token.cancelled() => Err(format!("{}: operation canceled", error_prefix)), + _ = close_token.cancelled() => Err(format!("{}: handle closed", error_prefix)), + res = tokio::time::timeout(timeout_duration(timeout_ms), op) => match res { + Ok(Ok(value)) => Ok(value), + Ok(Err(err)) => Err(format!("{}: {}", error_prefix, err)), + Err(_) => Err(format!("{} timed out", error_prefix)), + }, + } +} + +#[cfg(feature = "ffi-dataplane")] +fn leak_bytes(data: Vec) -> (*const std::ffi::c_uchar, u32) { + if data.is_empty() { + return (std::ptr::null(), 0); + } + let len = data.len() as u32; + let boxed = data.into_boxed_slice(); + (Box::into_raw(boxed) as *const std::ffi::c_uchar, len) +} + +#[cfg(feature = "ffi-dataplane")] +unsafe fn write_addr( + addr: SocketAddr, + out_ip: *mut *const std::ffi::c_char, + out_port: *mut std::ffi::c_ushort, +) -> Option<*mut std::ffi::c_char> { + let ip = into_ffi_ip_cstring(addr.ip())?; + unsafe { + *out_ip = ip as *const std::ffi::c_char; + *out_port = addr.port(); + } + Some(ip) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn has_live_ops() -> bool { + !DATA_PLANE_OPS.is_empty() +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn cancel_ops_for_handle(handle: u64) { + let ops = DATA_PLANE_OPS + .iter() + .filter(|entry| entry.target_handle == Some(handle)) + .map(|entry| entry.value().clone()) + .collect::>(); + for op in ops { + cancel_pending_op( + &op, + "data plane async op canceled because handle was closed", + ); + } +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn remove_ops_by_instance_ids(ids: &[Uuid]) { + if ids.is_empty() { + return; + } + + let op_handles = DATA_PLANE_OPS + .iter() + .filter(|entry| entry.instance_id.is_some_and(|id| ids.contains(&id))) + .map(|entry| *entry.key()) + .collect::>(); + for handle in op_handles { + if let Some((_, op)) = DATA_PLANE_OPS.remove(&handle) { + op.cancel_token.cancel(); + if let Ok(mut state) = op.state.lock() { + *state = DataPlaneAsyncOpState::Consumed; + op.ready.notify_all(); + } + } + } +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn data_plane_async_op_status(handle: u64) -> std::ffi::c_int { + let Some(op) = DATA_PLANE_OPS.get(&handle).map(|op| op.clone()) else { + return DATA_PLANE_OP_INVALID; + }; + let Ok(state) = op.state.lock() else { + return DATA_PLANE_OP_FAILED; + }; + state_status(&state) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn data_plane_async_op_wait(handle: u64, timeout_ms: u64) -> std::ffi::c_int { + let Some(op) = DATA_PLANE_OPS.get(&handle).map(|op| op.clone()) else { + return DATA_PLANE_OP_INVALID; + }; + let Ok(mut state) = op.state.lock() else { + return DATA_PLANE_OP_FAILED; + }; + if matches!(*state, DataPlaneAsyncOpState::Pending) && timeout_ms > 0 { + let timeout = Duration::from_millis(timeout_ms); + let Ok((next_state, _)) = op.ready.wait_timeout_while(state, timeout, |state| { + matches!(state, DataPlaneAsyncOpState::Pending) + }) else { + return DATA_PLANE_OP_FAILED; + }; + state = next_state; + } + state_status(&state) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn data_plane_async_op_cancel(handle: u64) -> std::ffi::c_int { + let Some(op) = DATA_PLANE_OPS.get(&handle).map(|op| op.clone()) else { + return DATA_PLANE_OP_INVALID; + }; + cancel_pending_op(&op, "data plane async op canceled"); + 0 +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn data_plane_async_op_free(handle: u64) -> std::ffi::c_int { + let Some((_, op)) = DATA_PLANE_OPS.remove(&handle) else { + return DATA_PLANE_OP_INVALID; + }; + op.cancel_token.cancel(); + if let Ok(mut state) = op.state.lock() { + *state = DataPlaneAsyncOpState::Consumed; + op.ready.notify_all(); + } + 0 +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn data_plane_free_bytes(ptr: *const std::ffi::c_uchar, len: u32) { + if ptr.is_null() { + return; + } + let slice = std::ptr::slice_from_raw_parts_mut(ptr as *mut std::ffi::c_uchar, len as usize); + unsafe { + drop(Box::from_raw(slice)); + } +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_connect_start( + inst_name: *const std::ffi::c_char, + dst_ip: *const std::ffi::c_char, + dst_port: std::ffi::c_ushort, + timeout_ms: u64, +) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else { + return 0; + }; + let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else { + return 0; + }; + let Some(instance_id) = get_instance_id(&inst_name) else { + set_error_msg("instance not found"); + return 0; + }; + let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else { + return 0; + }; + + let (handle, op) = new_op(DataPlaneAsyncOpKind::TcpConnect, Some(instance_id), None); + let op_for_task = op.clone(); + spawn_instance_runtime_op( + op, + instance_id, + timeout_ms, + move |runtime_for_result, remaining| async move { + run_with_cancel( + &op_for_task.cancel_token, + "failed to connect tcp data plane", + INSTANCE_MANAGER.data_plane_tcp_connect(&instance_id, dst_addr, remaining), + ) + .await + .map(|stream| { + let local_addr = stream.local_addr(); + DataPlaneAsyncOpResult::TcpConnect { + instance_id, + runtime: runtime_for_result, + stream, + local_addr, + } + }) + }, + ); + handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_connect_finish( + op_handle: u64, + out_local_ip: *mut *const std::ffi::c_char, + out_local_port: *mut std::ffi::c_ushort, +) -> u64 { + if out_local_ip.is_null() || out_local_port.is_null() { + set_error_msg("output pointer is null"); + return 0; + } + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::TcpConnect) else { + return 0; + }; + let DataPlaneAsyncOpResult::TcpConnect { + instance_id, + runtime, + stream, + local_addr, + } = result + else { + set_error_msg("data plane async op result type mismatch"); + return 0; + }; + let Some(_ip) = (unsafe { write_addr(local_addr, out_local_ip, out_local_port) }) else { + return 0; + }; + insert_tcp_stream_handle(instance_id, runtime, stream) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_bind_start( + inst_name: *const std::ffi::c_char, + local_port: std::ffi::c_ushort, + timeout_ms: u64, +) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else { + return 0; + }; + let Some(instance_id) = get_instance_id(&inst_name) else { + set_error_msg("instance not found"); + return 0; + }; + + let (handle, op) = new_op(DataPlaneAsyncOpKind::TcpBind, Some(instance_id), None); + let op_for_task = op.clone(); + spawn_instance_runtime_op( + op, + instance_id, + timeout_ms, + move |runtime_for_result, remaining| async move { + run_with_cancel( + &op_for_task.cancel_token, + "failed to bind tcp data plane", + INSTANCE_MANAGER.data_plane_tcp_bind(&instance_id, local_port, remaining), + ) + .await + .map(|listener| { + let local_addr = listener.local_addr(); + DataPlaneAsyncOpResult::TcpBind { + instance_id, + runtime: runtime_for_result, + listener, + local_addr, + } + }) + }, + ); + handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_bind_finish( + op_handle: u64, + out_local_ip: *mut *const std::ffi::c_char, + out_local_port: *mut std::ffi::c_ushort, +) -> u64 { + if out_local_ip.is_null() || out_local_port.is_null() { + set_error_msg("output pointer is null"); + return 0; + } + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::TcpBind) else { + return 0; + }; + let DataPlaneAsyncOpResult::TcpBind { + instance_id, + runtime, + listener, + local_addr, + } = result + else { + set_error_msg("data plane async op result type mismatch"); + return 0; + }; + let Some(_ip) = (unsafe { write_addr(local_addr, out_local_ip, out_local_port) }) else { + return 0; + }; + insert_tcp_listener_handle(instance_id, runtime, listener) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_accept_start(handle: u64, timeout_ms: u64) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some((listener, runtime, close_token, instance_id)) = get_tcp_listener(handle) else { + return 0; + }; + + let (op_handle, op) = new_op( + DataPlaneAsyncOpKind::TcpAccept, + Some(instance_id), + Some(handle), + ); + let runtime_for_result = runtime.clone(); + runtime.spawn(async move { + let result = async { + let mut listener = listener.lock().await; + let (stream, peer_addr) = run_io_with_cancel( + &op.cancel_token, + &close_token, + timeout_ms, + "tcp data plane accept", + listener.accept(), + ) + .await?; + let local_addr = stream.local_addr(); + Ok(DataPlaneAsyncOpResult::TcpAccept { + instance_id, + runtime: runtime_for_result, + stream, + local_addr, + peer_addr, + }) + } + .await; + complete_op(&op, result); + }); + op_handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_accept_finish( + op_handle: u64, + out_local_ip: *mut *const std::ffi::c_char, + out_local_port: *mut std::ffi::c_ushort, + out_peer_ip: *mut *const std::ffi::c_char, + out_peer_port: *mut std::ffi::c_ushort, +) -> u64 { + if out_local_ip.is_null() + || out_local_port.is_null() + || out_peer_ip.is_null() + || out_peer_port.is_null() + { + set_error_msg("output pointer is null"); + return 0; + } + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::TcpAccept) else { + return 0; + }; + let DataPlaneAsyncOpResult::TcpAccept { + instance_id, + runtime, + stream, + local_addr, + peer_addr, + } = result + else { + set_error_msg("data plane async op result type mismatch"); + return 0; + }; + let Some(local_ip) = (unsafe { write_addr(local_addr, out_local_ip, out_local_port) }) else { + return 0; + }; + if (unsafe { write_addr(peer_addr, out_peer_ip, out_peer_port) }).is_none() { + free_string(local_ip); + return 0; + } + insert_tcp_stream_handle(instance_id, runtime, stream) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_read_start(handle: u64, max_len: u32, timeout_ms: u64) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + if !validate_max_len(max_len) { + return 0; + } + let Some((halves, runtime, close_token, instance_id)) = get_tcp_stream_with_instance(handle) + else { + return 0; + }; + let (op_handle, op) = new_op( + DataPlaneAsyncOpKind::TcpRead, + Some(instance_id), + Some(handle), + ); + runtime.spawn(async move { + let result = read_tcp(halves, op.clone(), close_token, max_len, timeout_ms).await; + complete_op(&op, result); + }); + op_handle +} + +#[cfg(feature = "ffi-dataplane")] +async fn read_tcp( + halves: Arc, + op: Arc, + close_token: CancellationToken, + max_len: u32, + timeout_ms: u64, +) -> Result { + let mut buf = vec![0; max_len as usize]; + let mut rd = halves.read.lock().await; + let n = run_io_with_cancel( + &op.cancel_token, + &close_token, + timeout_ms, + "failed to read tcp data plane", + rd.read(&mut buf), + ) + .await?; + buf.truncate(n); + Ok(DataPlaneAsyncOpResult::TcpRead { data: buf }) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_read_finish( + op_handle: u64, + out_buf: *mut *const std::ffi::c_uchar, + out_len: *mut u32, +) -> std::ffi::c_int { + if out_buf.is_null() || out_len.is_null() { + set_error_msg("output pointer is null"); + return -1; + } + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::TcpRead) else { + return -1; + }; + let DataPlaneAsyncOpResult::TcpRead { data } = result else { + set_error_msg("data plane async op result type mismatch"); + return -1; + }; + let (ptr, len) = leak_bytes(data); + unsafe { + *out_buf = ptr; + *out_len = len; + } + len as std::ffi::c_int +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_tcp_write_start( + handle: u64, + buf: *const std::ffi::c_uchar, + len: u32, + timeout_ms: u64, +) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + if len > 0 && buf.is_null() { + set_error_msg("buf is null"); + return 0; + } + if !validate_write_len(len) { + return 0; + } + let Some((halves, runtime, close_token, instance_id)) = get_tcp_stream_with_instance(handle) + else { + return 0; + }; + let data = if len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(buf, len as usize) }.to_vec() + }; + let (op_handle, op) = new_op( + DataPlaneAsyncOpKind::TcpWrite, + Some(instance_id), + Some(handle), + ); + runtime.spawn(async move { + let result = write_tcp(halves, op.clone(), close_token, data, timeout_ms).await; + complete_op(&op, result); + }); + op_handle +} + +#[cfg(feature = "ffi-dataplane")] +async fn write_tcp( + halves: Arc, + op: Arc, + close_token: CancellationToken, + data: Vec, + timeout_ms: u64, +) -> Result { + let written = data.len(); + let mut wr = halves.write.lock().await; + run_io_with_cancel( + &op.cancel_token, + &close_token, + timeout_ms, + "failed to write tcp data plane", + wr.write_all(&data), + ) + .await?; + Ok(DataPlaneAsyncOpResult::TcpWrite { written }) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn data_plane_tcp_write_finish(op_handle: u64) -> std::ffi::c_int { + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::TcpWrite) else { + return -1; + }; + let DataPlaneAsyncOpResult::TcpWrite { written } = result else { + set_error_msg("data plane async op result type mismatch"); + return -1; + }; + usize_to_c_int(written, "tcp write byte count").unwrap_or(-1) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_udp_bind_start( + inst_name: *const std::ffi::c_char, + local_port: std::ffi::c_ushort, + timeout_ms: u64, +) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else { + return 0; + }; + let Some(instance_id) = get_instance_id(&inst_name) else { + set_error_msg("instance not found"); + return 0; + }; + + let (handle, op) = new_op(DataPlaneAsyncOpKind::UdpBind, Some(instance_id), None); + let op_for_task = op.clone(); + spawn_instance_runtime_op( + op, + instance_id, + timeout_ms, + move |runtime_for_result, remaining| async move { + run_with_cancel( + &op_for_task.cancel_token, + "failed to bind udp data plane", + INSTANCE_MANAGER.data_plane_udp_bind(&instance_id, local_port, remaining), + ) + .await + .map(|socket| { + let local_addr = socket.local_addr(); + DataPlaneAsyncOpResult::UdpBind { + instance_id, + runtime: runtime_for_result, + socket, + local_addr, + } + }) + }, + ); + handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_udp_bind_finish( + op_handle: u64, + out_local_ip: *mut *const std::ffi::c_char, + out_local_port: *mut std::ffi::c_ushort, +) -> u64 { + if out_local_ip.is_null() || out_local_port.is_null() { + set_error_msg("output pointer is null"); + return 0; + } + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::UdpBind) else { + return 0; + }; + let DataPlaneAsyncOpResult::UdpBind { + instance_id, + runtime, + socket, + local_addr, + } = result + else { + set_error_msg("data plane async op result type mismatch"); + return 0; + }; + let Some(_ip) = (unsafe { write_addr(local_addr, out_local_ip, out_local_port) }) else { + return 0; + }; + insert_udp_socket_handle(instance_id, runtime, socket) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_udp_send_to_start( + handle: u64, + dst_ip: *const std::ffi::c_char, + dst_port: std::ffi::c_ushort, + buf: *const std::ffi::c_uchar, + len: u32, + timeout_ms: u64, +) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + if len > 0 && buf.is_null() { + set_error_msg("buf is null"); + return 0; + } + if !validate_write_len(len) { + return 0; + } + let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else { + return 0; + }; + let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else { + return 0; + }; + let Some((socket, runtime, close_token, instance_id)) = get_udp_socket_with_instance(handle) + else { + return 0; + }; + let data = if len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(buf, len as usize) }.to_vec() + }; + + let (op_handle, op) = new_op( + DataPlaneAsyncOpKind::UdpSendTo, + Some(instance_id), + Some(handle), + ); + runtime.spawn(async move { + let result = run_io_with_cancel( + &op.cancel_token, + &close_token, + timeout_ms, + "failed to send udp data plane", + socket.send_to(&data, dst_addr), + ) + .await + .map(|sent| DataPlaneAsyncOpResult::UdpSendTo { sent }); + complete_op(&op, result); + }); + op_handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) fn data_plane_udp_send_to_finish(op_handle: u64) -> std::ffi::c_int { + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::UdpSendTo) else { + return -1; + }; + let DataPlaneAsyncOpResult::UdpSendTo { sent } = result else { + set_error_msg("data plane async op result type mismatch"); + return -1; + }; + usize_to_c_int(sent, "udp send byte count").unwrap_or(-1) +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_udp_recv_from_start( + handle: u64, + max_len: u32, + timeout_ms: u64, +) -> u64 { + let _data_plane_usage_guard = match enter_data_plane_operation() { + Some(guard) => guard, + None => return 0, + }; + if !validate_max_len(max_len) { + return 0; + } + let Some((socket, runtime, close_token, instance_id)) = get_udp_socket_with_instance(handle) + else { + return 0; + }; + let (op_handle, op) = new_op( + DataPlaneAsyncOpKind::UdpRecvFrom, + Some(instance_id), + Some(handle), + ); + runtime.spawn(async move { + let mut buf = vec![0; max_len as usize]; + let result = run_io_with_cancel( + &op.cancel_token, + &close_token, + timeout_ms, + "udp data plane receive", + socket.recv_from(&mut buf), + ) + .await + .map(|(n, peer_addr)| { + buf.truncate(n); + DataPlaneAsyncOpResult::UdpRecvFrom { + data: buf, + peer_addr, + } + }); + complete_op(&op, result); + }); + op_handle +} + +#[cfg(feature = "ffi-dataplane")] +pub(crate) unsafe fn data_plane_udp_recv_from_finish( + op_handle: u64, + out_buf: *mut *const std::ffi::c_uchar, + out_len: *mut u32, + out_ip: *mut *const std::ffi::c_char, + out_port: *mut std::ffi::c_ushort, +) -> std::ffi::c_int { + if out_buf.is_null() || out_len.is_null() || out_ip.is_null() || out_port.is_null() { + set_error_msg("output pointer is null"); + return -1; + } + let Some(result) = take_completed_op(op_handle, DataPlaneAsyncOpKind::UdpRecvFrom) else { + return -1; + }; + let DataPlaneAsyncOpResult::UdpRecvFrom { data, peer_addr } = result else { + set_error_msg("data plane async op result type mismatch"); + return -1; + }; + let Some(_ip) = (unsafe { write_addr(peer_addr, out_ip, out_port) }) else { + return -1; + }; + let (ptr, len) = leak_bytes(data); + unsafe { + *out_buf = ptr; + *out_len = len; + } + len as std::ffi::c_int +} + +#[cfg(all(test, feature = "ffi-dataplane"))] +mod tests { + use super::*; + + #[test] + fn cancel_marks_pending_op_failed_and_consumable() { + let (handle, _op) = new_op(DataPlaneAsyncOpKind::TcpRead, None, None); + + assert_eq!(data_plane_async_op_status(handle), DATA_PLANE_OP_PENDING); + assert_eq!(data_plane_async_op_cancel(handle), 0); + assert_eq!(data_plane_async_op_wait(handle, 0), DATA_PLANE_OP_FAILED); + assert!(take_completed_op(handle, DataPlaneAsyncOpKind::TcpRead).is_none()); + assert_eq!(data_plane_async_op_status(handle), DATA_PLANE_OP_INVALID); + } + + #[test] + fn max_len_limit_rejects_oversized_async_reads() { + assert!(validate_max_len(MAX_ASYNC_READ_LEN)); + assert!(!validate_max_len(MAX_ASYNC_READ_LEN + 1)); + } + + #[test] + fn write_len_limit_rejects_values_that_c_int_cannot_return() { + assert!(validate_write_len(MAX_ASYNC_WRITE_LEN)); + assert!(!validate_write_len(MAX_ASYNC_WRITE_LEN + 1)); + } + + #[test] + fn finish_return_count_must_fit_c_int() { + assert_eq!(usize_to_c_int(123, "test byte count"), Some(123)); + assert!(usize_to_c_int(std::ffi::c_int::MAX as usize + 1, "test byte count").is_none()); + } + + #[test] + fn free_consumes_ready_op_before_finish_can_take_it() { + let (handle, op) = new_op(DataPlaneAsyncOpKind::TcpRead, None, None); + complete_op(&op, Ok(DataPlaneAsyncOpResult::TcpRead { data: vec![1] })); + + assert_eq!(data_plane_async_op_free(handle), 0); + assert!(take_completed_op(handle, DataPlaneAsyncOpKind::TcpRead).is_none()); + assert_eq!(data_plane_async_op_status(handle), DATA_PLANE_OP_INVALID); + } +} diff --git a/easytier-contrib/easytier-ffi/src/instance_api.rs b/easytier-contrib/easytier-ffi/src/instance_api.rs index 420967f3..f39a78b0 100644 --- a/easytier-contrib/easytier-ffi/src/instance_api.rs +++ b/easytier-contrib/easytier-ffi/src/instance_api.rs @@ -112,6 +112,34 @@ pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> s 0 } +unsafe fn parse_instance_names( + inst_names: *const *const c_char, + length: usize, +) -> Option> { + if length == 0 { + return Some(Vec::new()); + } + if inst_names.is_null() { + set_error_msg("inst_names is null"); + return None; + } + + let names = unsafe { std::slice::from_raw_parts(inst_names, length) }; + let mut parsed = Vec::with_capacity(length); + for (index, &name) in names.iter().enumerate() { + if name.is_null() { + set_error_msg(&format!("inst_names[{}] is null", index)); + return None; + } + parsed.push( + unsafe { std::ffi::CStr::from_ptr(name) } + .to_string_lossy() + .into_owned(), + ); + } + Some(parsed) +} + /// # Safety /// Retain the network instance pub(crate) unsafe fn retain_network_instance( @@ -145,17 +173,8 @@ pub(crate) unsafe fn retain_network_instance( return 0; } - let inst_names = unsafe { - assert!(!inst_names.is_null()); - std::slice::from_raw_parts(inst_names, length) - .iter() - .map(|&name| { - assert!(!name.is_null()); - std::ffi::CStr::from_ptr(name) - .to_string_lossy() - .into_owned() - }) - .collect::>() + let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else { + return -1; }; let removed_ids = INSTANCE_MANAGER @@ -180,6 +199,54 @@ pub(crate) unsafe fn retain_network_instance( 0 } +/// # Safety +/// Delete named network instances. +pub(crate) unsafe fn delete_network_instance( + inst_names: *const *const std::ffi::c_char, + length: usize, +) -> std::ffi::c_int { + if in_config_server_callback() { + set_error_msg("cannot delete network instances from config server callback"); + return -1; + } + + wait_for_config_server_delivery(); + let _remote_mutation_guard = lock_remote_instance_mutation(); + let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() { + Ok(guard) => guard, + Err(err) => { + set_error_msg(&format!("failed to lock instance mutation: {}", err)); + return -1; + } + }; + + if length == 0 { + return 0; + } + + let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else { + return -1; + }; + + let removed_ids = inst_names + .iter() + .filter_map(|name| INSTANCE_NAME_ID_MAP.get(name).map(|id| *id.value())) + .collect::>(); + + if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) { + set_error_msg(&format!("failed to delete instances: {}", e)); + return -1; + } + + remove_config_server_tracked_instance_ids(&removed_ids); + remove_data_plane_handles_by_instance_ids(&removed_ids); + for name in inst_names { + INSTANCE_NAME_ID_MAP.remove(&name); + } + + 0 +} + /// # Safety /// Collect the network infos pub(crate) unsafe fn collect_network_infos( diff --git a/easytier-contrib/easytier-ffi/src/lib.rs b/easytier-contrib/easytier-ffi/src/lib.rs index acb22b54..a56012af 100644 --- a/easytier-contrib/easytier-ffi/src/lib.rs +++ b/easytier-contrib/easytier-ffi/src/lib.rs @@ -8,6 +8,7 @@ //! - `parse_config`: validate a TOML network config string. //! - `run_network_instance`: start one local network instance from TOML. //! - `retain_network_instance`: keep named instances and stop all others. +//! - `delete_network_instance`: stop named local network instances. //! - `collect_network_infos`: collect running instance info as key/value pairs. //! - `set_tun_fd`: attach a TUN file descriptor to a named instance. //! @@ -28,6 +29,8 @@ //! - `data_plane_udp_send_to`: send one UDP data-plane datagram. //! - `data_plane_udp_recv_from`: receive one UDP data-plane datagram. //! - `data_plane_udp_close`: close a UDP data-plane socket. +//! - `data_plane_*_start` / `data_plane_*_finish`: asynchronous data-plane operations. +//! - `data_plane_async_op_*`: poll, wait, cancel, and free asynchronous operations. //! //! Shared FFI helper APIs: //! - `get_error_msg`: copy the last FFI or config-server callback error message. @@ -35,6 +38,8 @@ mod config_server; mod data_plane; +#[cfg(feature = "ffi-dataplane")] +mod data_plane_async; mod error; mod instance_api; mod state; @@ -111,6 +116,30 @@ pub unsafe extern "C" fn retain_network_instance( unsafe { instance_api::retain_network_instance(inst_names, length) } } +/// Stop the named network instances. +/// +/// Passing `length == 0` is a no-op. When `length > 0`, `inst_names` must point +/// to an array of `length` non-null C strings. Unknown names are ignored. +/// Removed instances are also removed from the FFI name cache and any related +/// data-plane handles are closed. +/// +/// This API fails if called from a config-server event callback. +/// +/// # Safety +/// If `length > 0`, `inst_names` must be a non-null pointer to an array of +/// `length` non-null pointers to null-terminated UTF-8 strings. +/// +/// # Return +/// Returns `0` on success, or `-1` on failure. On failure, call +/// `get_error_msg` on the same thread to retrieve details. +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn delete_network_instance( + inst_names: *const *const c_char, + length: usize, +) -> c_int { + unsafe { instance_api::delete_network_instance(inst_names, length) } +} + /// Collect running network instance information. /// /// Writes up to `max_length` entries into `infos`. Each returned key is the @@ -490,6 +519,215 @@ pub extern "C" fn data_plane_udp_close(handle: u64) -> c_int { data_plane::data_plane_udp_close(handle) } +// ===== Async Data Plane API ===== + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub extern "C" fn data_plane_async_op_status(handle: u64) -> c_int { + data_plane_async::data_plane_async_op_status(handle) +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub extern "C" fn data_plane_async_op_wait(handle: u64, timeout_ms: u64) -> c_int { + data_plane_async::data_plane_async_op_wait(handle, timeout_ms) +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub extern "C" fn data_plane_async_op_cancel(handle: u64) -> c_int { + data_plane_async::data_plane_async_op_cancel(handle) +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub extern "C" fn data_plane_async_op_free(handle: u64) -> c_int { + data_plane_async::data_plane_async_op_free(handle) +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub extern "C" fn data_plane_free_bytes(ptr: *const c_uchar, len: u32) { + data_plane_async::data_plane_free_bytes(ptr, len) +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_connect_start( + inst_name: *const c_char, + dst_ip: *const c_char, + dst_port: c_ushort, + timeout_ms: u64, +) -> u64 { + unsafe { + data_plane_async::data_plane_tcp_connect_start(inst_name, dst_ip, dst_port, timeout_ms) + } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_connect_finish( + op_handle: u64, + out_local_ip: *mut *const c_char, + out_local_port: *mut c_ushort, +) -> u64 { + unsafe { + data_plane_async::data_plane_tcp_connect_finish(op_handle, out_local_ip, out_local_port) + } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_bind_start( + inst_name: *const c_char, + local_port: c_ushort, + timeout_ms: u64, +) -> u64 { + unsafe { data_plane_async::data_plane_tcp_bind_start(inst_name, local_port, timeout_ms) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_bind_finish( + op_handle: u64, + out_local_ip: *mut *const c_char, + out_local_port: *mut c_ushort, +) -> u64 { + unsafe { data_plane_async::data_plane_tcp_bind_finish(op_handle, out_local_ip, out_local_port) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_accept_start(handle: u64, timeout_ms: u64) -> u64 { + unsafe { data_plane_async::data_plane_tcp_accept_start(handle, timeout_ms) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_accept_finish( + op_handle: u64, + out_local_ip: *mut *const c_char, + out_local_port: *mut c_ushort, + out_peer_ip: *mut *const c_char, + out_peer_port: *mut c_ushort, +) -> u64 { + unsafe { + data_plane_async::data_plane_tcp_accept_finish( + op_handle, + out_local_ip, + out_local_port, + out_peer_ip, + out_peer_port, + ) + } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_read_start( + handle: u64, + max_len: u32, + timeout_ms: u64, +) -> u64 { + unsafe { data_plane_async::data_plane_tcp_read_start(handle, max_len, timeout_ms) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_read_finish( + op_handle: u64, + out_buf: *mut *const c_uchar, + out_len: *mut u32, +) -> c_int { + unsafe { data_plane_async::data_plane_tcp_read_finish(op_handle, out_buf, out_len) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_tcp_write_start( + handle: u64, + buf: *const c_uchar, + len: u32, + timeout_ms: u64, +) -> u64 { + unsafe { data_plane_async::data_plane_tcp_write_start(handle, buf, len, timeout_ms) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub extern "C" fn data_plane_tcp_write_finish(op_handle: u64) -> c_int { + data_plane_async::data_plane_tcp_write_finish(op_handle) +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_udp_bind_start( + inst_name: *const c_char, + local_port: c_ushort, + timeout_ms: u64, +) -> u64 { + unsafe { data_plane_async::data_plane_udp_bind_start(inst_name, local_port, timeout_ms) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_udp_bind_finish( + op_handle: u64, + out_local_ip: *mut *const c_char, + out_local_port: *mut c_ushort, +) -> u64 { + unsafe { data_plane_async::data_plane_udp_bind_finish(op_handle, out_local_ip, out_local_port) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_udp_send_to_start( + handle: u64, + dst_ip: *const c_char, + dst_port: c_ushort, + buf: *const c_uchar, + len: u32, + timeout_ms: u64, +) -> u64 { + unsafe { + data_plane_async::data_plane_udp_send_to_start( + handle, dst_ip, dst_port, buf, len, timeout_ms, + ) + } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub extern "C" fn data_plane_udp_send_to_finish(op_handle: u64) -> c_int { + data_plane_async::data_plane_udp_send_to_finish(op_handle) +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_udp_recv_from_start( + handle: u64, + max_len: u32, + timeout_ms: u64, +) -> u64 { + unsafe { data_plane_async::data_plane_udp_recv_from_start(handle, max_len, timeout_ms) } +} + +#[cfg(feature = "ffi-dataplane")] +#[cfg_attr(feature = "c-abi", unsafe(no_mangle))] +pub unsafe extern "C" fn data_plane_udp_recv_from_finish( + op_handle: u64, + out_buf: *mut *const c_uchar, + out_len: *mut u32, + out_ip: *mut *const c_char, + out_port: *mut c_ushort, +) -> c_int { + unsafe { + data_plane_async::data_plane_udp_recv_from_finish( + op_handle, out_buf, out_len, out_ip, out_port, + ) + } +} + // ===== Shared FFI Helper API ===== /// Return the last FFI error message. diff --git a/easytier-contrib/easytier-ffi/src/tests.rs b/easytier-contrib/easytier-ffi/src/tests.rs index e327ab6d..e658ca6f 100644 --- a/easytier-contrib/easytier-ffi/src/tests.rs +++ b/easytier-contrib/easytier-ffi/src/tests.rs @@ -304,6 +304,58 @@ fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() { remove_instance_name_ids(&[instance_id]); } +#[test] +fn delete_network_instance_removes_only_named_instances() { + let keep_id = Uuid::new_v4(); + let delete_id = Uuid::new_v4(); + let keep_name = format!("keep-{}", keep_id); + let delete_name = format!("delete-{}", delete_id); + + for (id, name) in [ + (keep_id, keep_name.clone()), + (delete_id, delete_name.clone()), + ] { + let cfg = TomlConfigLoader::default(); + cfg.set_id(id); + cfg.set_inst_name(name.clone()); + INSTANCE_MANAGER + .run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) + .unwrap(); + INSTANCE_NAME_ID_MAP.insert(name, id); + } + + let delete_name = CString::new(delete_name.clone()).unwrap(); + let inst_names = [delete_name.as_ptr()]; + assert_eq!( + unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) }, + 0 + ); + + assert_eq!(find_instance_id_by_name(&keep_name), Some(keep_id)); + assert!(find_instance_id_by_name(delete_name.to_str().unwrap()).is_none()); + + INSTANCE_MANAGER + .delete_network_instance(vec![keep_id]) + .unwrap(); + remove_instance_name_ids(&[keep_id]); +} + +#[test] +fn retain_and_delete_network_instance_reject_invalid_name_pointers() { + assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 1) }, -1); + assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 1) }, -1); + + let inst_names = [std::ptr::null()]; + assert_eq!( + unsafe { retain_network_instance(inst_names.as_ptr(), inst_names.len()) }, + -1 + ); + assert_eq!( + unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) }, + -1 + ); +} + #[test] fn ffi_remote_mutation_lock_uses_manager_lock() { let manager_guard = INSTANCE_MANAGER @@ -350,6 +402,7 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() { let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap(); assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1); assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1); + assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 0) }, -1); let url = CString::new("ring://test/token").unwrap(); let machine_id = CString::new("test-machine").unwrap(); assert_eq!( @@ -447,6 +500,34 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() { -1 ); assert_eq!(data_plane_udp_close(0), -1); + assert_eq!(data_plane_async_op_status(0), -2); + assert_eq!(data_plane_async_op_wait(0, 0), -2); + assert_eq!(data_plane_async_op_cancel(0), -2); + assert_eq!(data_plane_async_op_free(0), -2); + data_plane_free_bytes(std::ptr::null(), 0); + assert_eq!( + unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) }, + 0 + ); + assert_eq!( + unsafe { data_plane_tcp_bind_start(std::ptr::null(), 0, 0) }, + 0 + ); + assert_eq!(unsafe { data_plane_tcp_accept_start(0, 0) }, 0); + assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0); + assert_eq!( + unsafe { data_plane_tcp_write_start(0, std::ptr::null(), 0, 0) }, + 0 + ); + assert_eq!( + unsafe { data_plane_udp_bind_start(std::ptr::null(), 0, 0) }, + 0 + ); + assert_eq!( + unsafe { data_plane_udp_send_to_start(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) }, + 0 + ); + assert_eq!(unsafe { data_plane_udp_recv_from_start(0, 0, 0) }, 0); } } @@ -472,6 +553,21 @@ fn active_config_server_rejects_data_plane() { unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) }, -1 ); + assert_eq!( + unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) }, + 0 + ); + assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0); set_active_for_test(false); } + +#[cfg(feature = "ffi-dataplane")] +#[test] +fn async_op_invalid_handle_helpers_are_stable() { + assert_eq!(data_plane_async_op_status(u64::MAX), -2); + assert_eq!(data_plane_async_op_wait(u64::MAX, 1), -2); + assert_eq!(data_plane_async_op_cancel(u64::MAX), -2); + assert_eq!(data_plane_async_op_free(u64::MAX), -2); + data_plane_free_bytes(std::ptr::null(), 0); +}