mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-05 20:19:45 +00:00
793b57c2a1
* 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
452 lines
15 KiB
Kotlin
452 lines
15 KiB
Kotlin
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 <T> 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")
|
|
}
|
|
}
|