Merge upstream main into shared virtual NIC work

Port shared TUN ownership, routing, mobile source dispatch, and
Magic DNS route claims onto the native runtime-host architecture.

Preserve per-member lifecycle and add focused desktop, mobile, netlink,
GUI, FFI, and root integration validation for the merged code.
This commit is contained in:
KKRainbow
2026-08-13 00:56:52 +08:00
609 changed files with 130194 additions and 69014 deletions
+11 -6
View File
@@ -9,21 +9,26 @@ crate-type = ["cdylib", "rlib"]
[features]
default = ["c-abi", "ffi-dataplane"]
c-abi = []
ffi-dataplane = ["easytier/ffi-dataplane"]
ffi-dataplane = [
"easytier/ffi-dataplane",
"easytier-core/proxy-smoltcp-stack",
]
macos-ne = ["easytier/macos-ne"]
[dependencies]
easytier = { path = "../../easytier" }
easytier = { path = "../../easytier", features = ["tracing-log"] }
easytier-core = { path = "../../easytier-core" }
once_cell = "1.18.0"
dashmap = "6.0"
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
async-trait = "0.1"
log = "0.4"
percent-encoding = "2.3"
url = "2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
uuid = "1.17.0"
tokio-util = "0.7"
[build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
"win7",
] }
@@ -0,0 +1,108 @@
# Native data-plane ABI v3
The native data-plane ABI is a thin adapter over the instance-owned
`DataPlaneSession`. It does not own sockets, operation state, completion
queues, routing policy, or timeouts.
## Conventions
- Every immediate call returns `0` on success or a negative
`DataPlaneErrorKind` value on failure.
- `data_plane_completion_wait` returns `1` when a completion is ready, `0` on
timeout or session close, and a negative error value on failure.
- `data_plane_completion_drain` returns a non-negative descriptor count or a
negative error value.
- Handle zero is invalid.
- `timeout_ms == UINT64_MAX` means no deadline.
- TCP connect/bind/accept and UDP bind timeouts start when submission is
accepted.
- TCP streams and UDP sockets have persistent read and write deadlines.
`data_plane_resource_deadline_set` replaces the selected directions'
deadlines immediately, including for active operations. An expired deadline
remains expired until it is replaced or cleared with `UINT64_MAX`.
- Deadline direction `1` selects reads, `2` selects writes, and `3` selects
both.
- Request and write bytes are copied before a submit call returns.
- Socket-address fields use native-endian integers. Address bytes are in
network order. ABI v3 accepts IPv4 only.
`DataPlaneSocketAddr` is:
```c
typedef struct {
uint16_t family; /* 4 */
uint16_t port;
uint8_t address[16]; /* IPv4 uses the first four bytes */
} DataPlaneSocketAddr;
```
`DataPlaneCompletion` is:
```c
typedef struct {
uint64_t operation_id;
uint16_t operation_kind;
uint16_t status; /* 0 or DataPlaneErrorKind */
} DataPlaneCompletion;
```
## Lifecycle
One native session may be open for an EasyTier instance at a time:
```text
data_plane_session_open
-> set resource deadlines
-> submit operations
-> completion_wait
-> completion_drain
-> typed result_take
-> resource_close / operation_free
data_plane_session_close
```
Closing a native session cancels and discards its outstanding operations and
resources and wakes a thread blocked in `data_plane_completion_wait`.
The resource and operation IDs returned by the ABI belong to that session.
They must always be passed together with the same session handle.
## Completion and result ownership
Submission returns an operation ID immediately. Completion descriptors carry
only the operation ID, operation kind, and terminal status. Draining a
descriptor makes its typed result available but does not consume it.
`data_plane_result_size` reports the TCP-read or UDP-receive payload size.
Typed result-take functions consume the result exactly once. If a supplied
buffer is too small, they return `-BufferTooSmall` and leave the result
available for a later call.
Call `data_plane_operation_free` when a drained result is intentionally
abandoned. Call `data_plane_resource_close` for TCP streams, listeners, and
UDP sockets.
## Operation kinds
| Value | Operation |
| ---: | --- |
| 1 | TCP connect |
| 2 | TCP bind |
| 3 | TCP accept |
| 4 | TCP read |
| 5 | TCP write |
| 6 | UDP bind |
| 7 | UDP receive |
| 8 | UDP send |
The exported function families are:
- `data_plane_tcp_*_submit`
- `data_plane_udp_*_submit`
- `data_plane_resource_deadline_set`
- `data_plane_completion_wait`
- `data_plane_completion_drain`
- `data_plane_*_result_take`
- `data_plane_operation_cancel`
- `data_plane_operation_free`
- `data_plane_resource_close`
+8
View File
@@ -0,0 +1,8 @@
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
thunk::thunk();
}
}
@@ -1,429 +0,0 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
@@ -1,138 +0,0 @@
# 1. Go FFI Demo
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
Run from the repository root:
```sh
cargo build -p easytier-ffi --features ffi-dataplane
```
The demo loads the debug library by default:
```text
target/debug/libeasytier_ffi.so
```
To use another library path, export `EASYTIER_FFI_LIB=/path/to/libeasytier_ffi.so`.
## 1.2. Configure the EasyTier config
`EASYTIER_FFI_CONFIG` is a string of the EasyTier config in TOML format which is passed to the FFI library. For example:
```sh
export EASYTIER_FFI_CONFIG='instance_name = "default"
ipv4 = "10.0.0.1"
[network_identity]
network_name = "testnet"
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"
'
```
You should configure with your own real values.
Set the local instance name and a SSH server target to connect through EasyTier:
```sh
export EASYTIER_FFI_INSTANCE=default
export EASYTIER_FFI_TARGET=10.0.0.2:22
```
To run the TCP listen integration test in the same `go test` process as the SSH
test, use a separate instance name and config:
```sh
export EASYTIER_FFI_LISTEN_CONFIG='instance_name = "listener"
ipv4 = "10.0.0.3"
[network_identity]
network_name = "testnet"
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
```
## 1.3. Run the demo
`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 ./...
```
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.
The synchronous wrapper also exposes `CallJSONRPC(service, method, domain,
payload)` for non-lifecycle EasyTier RPCs. For example,
`CallJSONRPC("api.logger.LoggerRpcService", "get_logger_config", "", "{}")`
returns the logger config as protobuf JSON. Instance lifecycle management RPCs
are intentionally filtered; use the dedicated FFI APIs for starting and
stopping instances.
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-..."
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.
@@ -1,593 +0,0 @@
package easytierffi
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"
"unsafe"
"github.com/go-webgpu/goffi/ffi"
"github.com/go-webgpu/goffi/types"
)
const defaultTimeout = 30 * time.Second
type Native struct {
lib unsafe.Pointer
runNetworkInstance symCall
callJSONRPC symCall
getErrorMsg symCall
freeString symCall
tcpConnect symCall
tcpBind symCall
tcpAccept symCall
tcpRead symCall
tcpWrite symCall
tcpClose symCall
tcpListenerClose symCall
}
type Conn struct {
native *Native
handle uint64
local net.Addr
remote net.Addr
closed atomic.Bool
rd atomicDeadline
wd atomicDeadline
}
type Listener struct {
native *Native
handle uint64
addr net.Addr
closed atomic.Bool
}
type symCall struct {
fn unsafe.Pointer
cif types.CallInterface
}
type atomicDeadline struct{ v atomic.Int64 }
type timeoutError string
func Open(path string) (*Native, error) {
lib, err := ffi.LoadLibrary(path)
if err != nil {
return nil, err
}
n := &Native{lib: lib}
if err := n.bind(); err != nil {
ffi.FreeLibrary(lib)
return nil, err
}
return n, nil
}
func (n *Native) Close() error {
if n.lib == nil {
return nil
}
ffi.FreeLibrary(n.lib)
n.lib = nil
return nil
}
func (n *Native) 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 *Native) CallJSONRPC(serviceName, methodName, domainName, payloadJSON string) (string, error) {
defer pinErrorThread()()
service := cString(serviceName)
method := cString(methodName)
payload := cString(payloadJSON)
servicePtr := unsafe.Pointer(&service[0])
methodPtr := unsafe.Pointer(&method[0])
payloadPtr := unsafe.Pointer(&payload[0])
var domain []byte
var domainPtr unsafe.Pointer
if domainName != "" {
domain = cString(domainName)
domainPtr = unsafe.Pointer(&domain[0])
}
var response unsafe.Pointer
responseArg := unsafe.Pointer(&response)
var ret int32
err := n.callJSONRPC.call(
unsafe.Pointer(&ret),
unsafe.Pointer(&servicePtr),
unsafe.Pointer(&methodPtr),
unsafe.Pointer(&domainPtr),
unsafe.Pointer(&payloadPtr),
unsafe.Pointer(&responseArg),
)
runtime.KeepAlive(service)
runtime.KeepAlive(method)
runtime.KeepAlive(domain)
runtime.KeepAlive(payload)
if err != nil {
return "", err
}
if ret != 0 {
return "", n.lastError()
}
if response == nil {
return "", errors.New("easytier ffi JSON RPC returned nil response")
}
defer func() { _ = n.freeCString(response) }()
return readCString(response), nil
}
func (n *Native) 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
}
timeout := defaultTimeout
if deadline, ok := ctx.Deadline(); ok {
timeout = time.Until(deadline)
}
if timeout <= 0 {
return nil, context.DeadlineExceeded
}
if err := ctx.Err(); err != nil {
return nil, err
}
handle, local, err := n.tcpConnectTo(instance, ip.String(), uint16(port), timeout)
if err != nil {
return nil, err
}
return &Conn{native: n, handle: handle, local: local, remote: &net.TCPAddr{IP: ip, Port: port}}, nil
}
func (n *Native) 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
}
timeout := defaultTimeout
if deadline, ok := ctx.Deadline(); ok {
timeout = time.Until(deadline)
}
if timeout <= 0 {
return nil, context.DeadlineExceeded
}
if err := ctx.Err(); err != nil {
return nil, err
}
handle, local, err := n.tcpBindTo(instance, uint16(port), timeout)
if err != nil {
return nil, err
}
return &Listener{native: n, handle: handle, addr: local}, nil
}
func (c *Conn) Read(b []byte) (int, error) {
if c.closed.Load() {
return 0, net.ErrClosed
}
n, err := c.native.tcpReadFrom(c.handle, b, c.rd.timeout(defaultTimeout))
if err != nil {
return 0, opError("read", c.remote, err)
}
if n == 0 {
return 0, io.EOF
}
return n, nil
}
func (c *Conn) Write(b []byte) (int, error) {
if c.closed.Load() {
return 0, net.ErrClosed
}
n, err := c.native.tcpWriteTo(c.handle, b, c.wd.timeout(defaultTimeout))
if err != nil {
return 0, opError("write", c.remote, err)
}
return n, nil
}
func (c *Conn) Close() error {
if !c.closed.CompareAndSwap(false, true) {
return net.ErrClosed
}
return c.native.tcpCloseHandle(c.handle)
}
func (c *Conn) LocalAddr() net.Addr { return c.local }
func (c *Conn) RemoteAddr() net.Addr { return c.remote }
func (c *Conn) SetDeadline(t time.Time) error { c.rd.set(t); c.wd.set(t); return nil }
func (c *Conn) SetReadDeadline(t time.Time) error { c.rd.set(t); return nil }
func (c *Conn) SetWriteDeadline(t time.Time) error { c.wd.set(t); return nil }
func (l *Listener) Accept() (net.Conn, error) {
if l.closed.Load() {
return nil, net.ErrClosed
}
for {
handle, local, peer, err := l.native.tcpAcceptFrom(l.handle, defaultTimeout)
if err == nil {
return &Conn{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 *Listener) Close() error {
if !l.closed.CompareAndSwap(false, true) {
return net.ErrClosed
}
return l.native.tcpListenerCloseHandle(l.handle)
}
func (l *Listener) Addr() net.Addr { return l.addr }
func (n *Native) bind() error {
return errors.Join(
n.bindSym(&n.runNetworkInstance, "run_network_instance", types.SInt32TypeDescriptor, types.PointerTypeDescriptor),
n.bindSym(&n.callJSONRPC, "call_json_rpc", types.SInt32TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
n.bindSym(&n.getErrorMsg, "get_error_msg", types.VoidTypeDescriptor, types.PointerTypeDescriptor),
n.bindSym(&n.freeString, "free_string", types.VoidTypeDescriptor, types.PointerTypeDescriptor),
n.bindSym(&n.tcpConnect, "data_plane_tcp_connect", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
n.bindSym(&n.tcpBind, "data_plane_tcp_bind", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
n.bindSym(&n.tcpAccept, "data_plane_tcp_accept", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
n.bindSym(&n.tcpRead, "data_plane_tcp_read", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor),
n.bindSym(&n.tcpWrite, "data_plane_tcp_write", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, 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),
)
}
func (n *Native) 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 (s *symCall) call(ret unsafe.Pointer, args ...unsafe.Pointer) error {
// `ffi.CallFunction` and libffi `ffi_call` are safe to invoke concurrently
// because `cif` is prepared once during binding and only read afterwards.
return ffi.CallFunction(&s.cif, s.fn, ret, args)
}
func (n *Native) tcpConnectTo(instance, ip string, port uint16, timeout time.Duration) (uint64, *net.TCPAddr, error) {
defer pinErrorThread()()
inst := cString(instance)
dst := cString(ip)
instPtr := unsafe.Pointer(&inst[0])
dstPtr := unsafe.Pointer(&dst[0])
timeoutMS := uint64(timeout / time.Millisecond)
var handle uint64
var outIP unsafe.Pointer
outIPArg := unsafe.Pointer(&outIP)
var outPort uint16
outPortArg := unsafe.Pointer(&outPort)
err := n.tcpConnect.call(
unsafe.Pointer(&handle),
unsafe.Pointer(&instPtr),
unsafe.Pointer(&dstPtr),
unsafe.Pointer(&port),
unsafe.Pointer(&timeoutMS),
unsafe.Pointer(&outIPArg),
unsafe.Pointer(&outPortArg),
)
runtime.KeepAlive(inst)
runtime.KeepAlive(dst)
if err != nil {
return 0, nil, err
}
if handle == 0 {
return 0, nil, n.lastError()
}
return handle, n.takeTCPAddr(outIP, outPort), nil
}
func (n *Native) tcpBindTo(instance string, port uint16, timeout time.Duration) (uint64, *net.TCPAddr, error) {
defer pinErrorThread()()
inst := cString(instance)
instPtr := unsafe.Pointer(&inst[0])
timeoutMS := uint64(timeout / time.Millisecond)
var handle uint64
var outIP unsafe.Pointer
outIPArg := unsafe.Pointer(&outIP)
var outPort uint16
outPortArg := unsafe.Pointer(&outPort)
err := n.tcpBind.call(
unsafe.Pointer(&handle),
unsafe.Pointer(&instPtr),
unsafe.Pointer(&port),
unsafe.Pointer(&timeoutMS),
unsafe.Pointer(&outIPArg),
unsafe.Pointer(&outPortArg),
)
runtime.KeepAlive(inst)
if err != nil {
return 0, nil, err
}
if handle == 0 {
return 0, nil, n.lastError()
}
return handle, n.takeTCPAddr(outIP, outPort), nil
}
func (n *Native) tcpAcceptFrom(handle uint64, timeout time.Duration) (uint64, *net.TCPAddr, *net.TCPAddr, error) {
defer pinErrorThread()()
timeoutMS := uint64(timeout / time.Millisecond)
var stream uint64
var outLocalIP unsafe.Pointer
outLocalIPArg := unsafe.Pointer(&outLocalIP)
var outLocalPort uint16
outLocalPortArg := unsafe.Pointer(&outLocalPort)
var outPeerIP unsafe.Pointer
outPeerIPArg := unsafe.Pointer(&outPeerIP)
var outPeerPort uint16
outPeerPortArg := unsafe.Pointer(&outPeerPort)
err := n.tcpAccept.call(
unsafe.Pointer(&stream),
unsafe.Pointer(&handle),
unsafe.Pointer(&timeoutMS),
unsafe.Pointer(&outLocalIPArg),
unsafe.Pointer(&outLocalPortArg),
unsafe.Pointer(&outPeerIPArg),
unsafe.Pointer(&outPeerPortArg),
)
if err != nil {
return 0, nil, nil, err
}
if stream == 0 {
return 0, nil, nil, n.lastError()
}
return stream, n.takeTCPAddr(outLocalIP, outLocalPort), n.takeTCPAddr(outPeerIP, outPeerPort), nil
}
func (n *Native) tcpReadFrom(handle uint64, buf []byte, timeout time.Duration) (int, error) {
if len(buf) == 0 {
return 0, nil
}
defer pinErrorThread()()
var ret int32
bufPtr := unsafe.Pointer(&buf[0])
length := uint32(len(buf))
timeoutMS := uint64(timeout / time.Millisecond)
err := n.tcpRead.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle), unsafe.Pointer(&bufPtr), unsafe.Pointer(&length), unsafe.Pointer(&timeoutMS))
runtime.KeepAlive(buf)
if err != nil {
return 0, err
}
if ret < 0 {
return 0, n.lastError()
}
return int(ret), nil
}
func (n *Native) tcpWriteTo(handle uint64, buf []byte, timeout time.Duration) (int, error) {
if len(buf) == 0 {
return 0, nil
}
defer pinErrorThread()()
var ret int32
bufPtr := unsafe.Pointer(&buf[0])
length := uint32(len(buf))
timeoutMS := uint64(timeout / time.Millisecond)
err := n.tcpWrite.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle), unsafe.Pointer(&bufPtr), unsafe.Pointer(&length), unsafe.Pointer(&timeoutMS))
runtime.KeepAlive(buf)
if err != nil {
return 0, err
}
if ret < 0 {
return 0, n.lastError()
}
return int(ret), nil
}
func (n *Native) 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 *Native) 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
}
// pinErrorThread ties an FFI op to the get_error_msg that reads its result: the
// Rust side stores the last error in a thread-local, so the goroutine must not
// migrate to another OS thread between the two calls. Use as `defer pinErrorThread()()`
// at the start of any wrapper that reports failures through lastError.
func pinErrorThread() func() {
runtime.LockOSThread()
return runtime.UnlockOSThread
}
func (n *Native) 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 strings.Contains(msg, "timed out") {
return timeoutError(msg)
}
return errors.New(msg)
}
func (n *Native) freeCString(ptr unsafe.Pointer) error {
if ptr == nil {
return nil
}
return n.freeString.call(nil, unsafe.Pointer(&ptr))
}
func (n *Native) 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 (d *atomicDeadline) set(t time.Time) {
if t.IsZero() {
d.v.Store(0)
return
}
d.v.Store(t.UnixNano())
}
func (d *atomicDeadline) timeout(fallback time.Duration) time.Duration {
ns := d.v.Load()
if ns == 0 {
return fallback
}
remaining := time.Until(time.Unix(0, ns))
if remaining <= 0 {
return time.Millisecond
}
return remaining
}
func (e timeoutError) Error() string { return string(e) }
func (e timeoutError) Timeout() bool { return true }
func (e timeoutError) Temporary() bool { return true }
func opError(op string, addr net.Addr, err error) error {
return &net.OpError{Op: op, Net: "easytier", Addr: addr, Err: err}
}
func parseIPPort(address string) (net.IP, int, error) {
host, portStr, err := net.SplitHostPort(address)
if err != nil {
return nil, 0, err
}
ip := net.ParseIP(host)
if ip == nil {
return nil, 0, fmt.Errorf("easytier ffi requires an IP address, got %q", host)
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return nil, 0, err
}
return ip, int(port), nil
}
func parseListenPort(address string) (int, error) {
host, portStr, err := net.SplitHostPort(address)
if err != nil {
return 0, err
}
if host != "" {
ip := net.ParseIP(host)
if ip == nil {
return 0, fmt.Errorf("easytier ffi requires an IP address, got %q", host)
}
if !ip.IsUnspecified() {
return 0, fmt.Errorf("easytier ffi listen address must be unspecified, got %q", host)
}
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return 0, err
}
return int(port), nil
}
func cString(s string) []byte {
if strings.ContainsRune(s, 0) {
panic("easytier ffi string contains NUL")
}
return append([]byte(s), 0)
}
func readCString(ptr unsafe.Pointer) string {
if ptr == nil {
return ""
}
var b []byte
for p := uintptr(ptr); ; p++ {
c := *(*byte)(unsafe.Pointer(p))
if c == 0 {
return string(b)
}
b = append(b, c)
}
}
func defaultLibraryPath() string {
if p := os.Getenv("EASYTIER_FFI_LIB"); p != "" {
return p
}
switch runtime.GOOS {
case "darwin":
return "../../../../target/debug/libeasytier_ffi.dylib"
case "windows":
return "..\\..\\..\\..\\target\\debug\\easytier_ffi.dll"
default:
return "../../../../target/debug/libeasytier_ffi.so"
}
}
var _ net.Conn = (*Conn)(nil)
var _ net.Listener = (*Listener)(nil)
File diff suppressed because it is too large Load Diff
@@ -1,360 +0,0 @@
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():
}
}
@@ -1,140 +0,0 @@
package easytierffi
import (
"context"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
"testing"
"time"
)
func TestSSHIntegration(t *testing.T) {
config := os.Getenv("EASYTIER_FFI_CONFIG")
instance := os.Getenv("EASYTIER_FFI_INSTANCE")
target := os.Getenv("EASYTIER_FFI_TARGET")
if config == "" || instance == "" || target == "" {
t.Skip("set EASYTIER_FFI_CONFIG, EASYTIER_FFI_INSTANCE and EASYTIER_FFI_TARGET to run integration test")
}
n, err := Open(defaultLibraryPath())
if err != nil {
t.Fatal(err)
}
defer n.Close()
if err := n.RunNetworkInstance(config); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
var lastErr error
for attempt := 1; ctx.Err() == nil; attempt++ {
conn, err := n.DialContext(ctx, instance, "tcp", target)
if err != nil {
lastErr = err
t.Logf("attempt %d: dial failed: %v", attempt, err)
time.Sleep(3 * time.Second)
continue
}
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
buf := make([]byte, 128)
nn, err := conn.Read(buf)
_ = conn.Close()
if err != nil {
lastErr = err
t.Logf("attempt %d: read failed: %v", attempt, err)
time.Sleep(3 * time.Second)
continue
}
banner := string(buf[:nn])
if !strings.HasPrefix(banner, "SSH-") {
t.Fatalf("attempt %d: expected SSH banner, got %q", attempt, banner)
}
t.Logf("attempt %d: got banner %q", attempt, strings.TrimRight(banner, "\r\n"))
return
}
t.Fatalf("never got SSH banner, last err: %v", lastErr)
}
func TestTCPListenIntegration(t *testing.T) {
config := os.Getenv("EASYTIER_FFI_LISTEN_CONFIG")
instance := os.Getenv("EASYTIER_FFI_LISTEN_INSTANCE")
listenPort := os.Getenv("EASYTIER_FFI_LISTEN_PORT")
if config == "" || instance == "" || listenPort == "" {
t.Skip("set EASYTIER_FFI_LISTEN_CONFIG, EASYTIER_FFI_LISTEN_INSTANCE and EASYTIER_FFI_LISTEN_PORT to run integration test")
}
port, err := strconv.ParseUint(listenPort, 10, 16)
if err != nil {
t.Fatal(err)
}
n, err := Open(defaultLibraryPath())
if err != nil {
t.Fatal(err)
}
defer n.Close()
if err := n.RunNetworkInstance(config); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
// Data-plane readiness is asynchronous: the instance must finish starting
// before the data plane accepts binds. Retry until ready or ctx expires.
var listener net.Listener
for attempt := 1; ; attempt++ {
listener, err = n.ListenContext(ctx, instance, "tcp", net.JoinHostPort("0.0.0.0", strconv.Itoa(int(port))))
if err == nil {
break
}
if ctx.Err() != nil {
t.Fatalf("bind never succeeded, last err: %v", err)
}
t.Logf("attempt %d: bind failed: %v", attempt, err)
time.Sleep(3 * time.Second)
}
t.Logf("listening on %s; connect from another EasyTier peer and send ping", listener.Addr())
accepted := make(chan error, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
accepted <- err
return
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
buf := make([]byte, 4)
if _, err := io.ReadFull(conn, buf); err != nil {
accepted <- err
return
}
if string(buf) != "ping" {
accepted <- fmt.Errorf("expected %q, got %q", "ping", string(buf))
return
}
_, err = conn.Write([]byte("pong"))
accepted <- err
}()
select {
case err := <-accepted:
_ = listener.Close()
if err != nil {
t.Fatal(err)
}
case <-ctx.Done():
_ = listener.Close()
t.Fatal(ctx.Err())
}
}
@@ -1,5 +0,0 @@
module easytierffi-example
go 1.25
require github.com/go-webgpu/goffi v0.4.1
@@ -13,18 +13,14 @@ use easytier::{
MachineIdOptions,
config::{ConfigLoader as _, TomlConfigLoader},
},
tunnel::TunnelScheme,
web_client::{WebClient, WebClientHooks, run_web_client},
web_client::{WebClient, WebClientHooks, parse_config_server_endpoint, run_web_client},
};
use uuid::Uuid;
use crate::{
data_plane::remove_data_plane_handles_by_instance_ids,
data_plane::remove_data_plane_sessions_by_instance_ids,
error::set_error_msg,
state::{
ASYNC_RUNTIME, INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP,
lock_remote_instance_mutation, remove_instance_name_ids,
},
state::{ffi_context, resolve_instance_id_by_name},
strings::{c_str_to_string, optional_c_str_to_string},
types::ConfigServerEventCallback,
};
@@ -76,37 +72,9 @@ pub fn validate_config_server_client_options(
return Err("machine_id is empty".to_string());
}
let config_server_url = match url::Url::parse(config_server_url_s) {
Ok(url) => url,
Err(_) => format!(
"udp://config-server.easytier.cn:22020/{}",
config_server_url_s
)
.parse()
.map_err(|err| format!("failed to parse config server URL: {}", err))?,
};
TunnelScheme::try_from(&config_server_url).map_err(|_| {
format!(
"unsupported config server scheme: {}",
config_server_url.scheme()
)
})?;
let token = config_server_url
.path_segments()
.and_then(|mut segments| segments.next_back())
.map(|segment| percent_encoding::percent_decode_str(segment).decode_utf8())
.transpose()
.map_err(|err| format!("failed to decode config server token: {}", err))?
.map(|token| token.to_string())
.unwrap_or_default();
if token.is_empty() {
return Err("empty token".to_string());
}
Ok(())
parse_config_server_endpoint(config_server_url_s)
.map(|_| ())
.map_err(|error| error.to_string())
}
struct ManagedConfigServerClient {
@@ -150,7 +118,8 @@ impl ManagedConfigServerClientHooks {
}
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
if let Some(existing_id) = INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id)
if let Some(existing_id) =
resolve_instance_id_by_name(inst_name).map_err(|error| error.to_string())?
&& existing_id != inst_id
{
return Err(format!("instance name {} already exists", inst_name));
@@ -159,13 +128,6 @@ impl ManagedConfigServerClientHooks {
Ok(())
}
fn commit_instance_name(&self, inst_name: String, inst_id: Uuid) -> Result<(), String> {
INSTANCE_NAME_ID_MAP.retain(|_, existing_id| *existing_id != inst_id);
self.validate_instance_name(&inst_name, inst_id)?;
INSTANCE_NAME_ID_MAP.insert(inst_name, inst_id);
Ok(())
}
pub(crate) fn start_stopping(&self) -> Vec<Uuid> {
let _delivery_guard = if in_config_server_callback() {
None
@@ -199,11 +161,15 @@ impl ManagedConfigServerClientHooks {
let Some(callback) = self.callback else {
return Ok(());
};
let instance_name = INSTANCE_MANAGER
.get_instance_name(&instance_id)
let instance_name = ffi_context()
.manager
.instance(instance_id)
.map(|instance| instance.instance_name().to_owned())
.unwrap_or_default();
let network_name = INSTANCE_MANAGER
.get_network_name(&instance_id)
let network_name = ffi_context()
.manager
.config(instance_id)
.map(|config| config.get_network_identity().network_name)
.unwrap_or_default();
let event_json = serde_json::json!({
"event": event,
@@ -263,75 +229,27 @@ impl WebClientHooks for ManagedConfigServerClientHooks {
.callback_delivery
.lock()
.map_err(|err| err.to_string())?;
let Some(inst_name) = INSTANCE_MANAGER.get_instance_name(id) else {
if !self.stopping.load(Ordering::Acquire) {
return Err(format!("instance {} not found after start", id));
}
return Ok(());
if self.stopping.load(Ordering::Acquire) {
return Err("config server client is stopping".to_string());
}
let Some(inst_name) = ffi_context()
.manager
.instance(*id)
.map(|instance| instance.instance_name().to_owned())
else {
return Err(format!("instance {} not found after start", id));
};
{
let _mutation_guard = INSTANCE_MUTATION_LOCK
.lock()
.map_err(|err| err.to_string())?;
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
if !self.stopping.load(Ordering::Acquire) {
return Err(format!("instance {} not found after start", id));
}
return Ok(());
}
let should_delete = {
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
if self.stopping.load(Ordering::Acquire) {
true
} else {
guard.insert(*id);
false
}
};
if should_delete {
if let Err(err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
return Err(err.to_string());
}
remove_instance_name_ids(&[*id]);
return Ok(());
}
if self.stopping.load(Ordering::Acquire) {
self.remove_tracked_instance_ids(&[*id])?;
remove_instance_name_ids(&[*id]);
return Ok(());
}
if let Err(err) = self.commit_instance_name(inst_name.clone(), *id) {
self.remove_tracked_instance_ids(&[*id])?;
if let Err(delete_err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
return Err(format!(
"{}; failed to delete duplicate instance: {}",
err, delete_err
));
}
return Err(err);
}
if self.stopping.load(Ordering::Acquire) {
self.remove_tracked_instance_ids(&[*id])?;
remove_instance_name_ids(&[*id]);
return Ok(());
}
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
self.remove_tracked_instance_ids(&[*id])?;
remove_instance_name_ids(&[*id]);
return Err(format!(
"instance {} was removed before post-run completed",
id
));
}
self.instance_ids
.lock()
.map_err(|err| err.to_string())?
.insert(*id);
if let Err(error) = self.validate_instance_name(&inst_name, *id) {
self.remove_tracked_instance_ids(&[*id])?;
return Err(error);
}
remove_data_plane_handles_by_instance_ids(&[*id]);
remove_data_plane_sessions_by_instance_ids(&[*id]);
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
self.note_callback_error(err);
@@ -340,15 +258,8 @@ impl WebClientHooks for ManagedConfigServerClientHooks {
}
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
let removed_ids = {
let _mutation_guard = INSTANCE_MUTATION_LOCK
.lock()
.map_err(|err| err.to_string())?;
let removed_ids = self.remove_tracked_instance_ids(ids)?;
remove_instance_name_ids(ids);
remove_data_plane_handles_by_instance_ids(&removed_ids);
removed_ids
};
let removed_ids = self.remove_tracked_instance_ids(ids)?;
remove_data_plane_sessions_by_instance_ids(&removed_ids);
for id in removed_ids {
if let Err(err) = self.emit_event("delete_network_instance", id) {
@@ -485,12 +396,12 @@ pub(crate) unsafe fn start_config_server_client(
drop(data_plane_usage_guard);
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
let client = match ASYNC_RUNTIME.block_on(run_web_client(
let client = match ffi_context().runtime.block_on(run_web_client(
&config_server_url,
config_server_machine_id_options(machine_id),
hostname,
secure_mode,
INSTANCE_MANAGER.clone(),
ffi_context().manager.clone(),
Some(hooks.clone()),
)) {
Ok(client) => client,
@@ -511,7 +422,7 @@ pub(crate) fn stop_config_server_client() -> c_int {
return -1;
}
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
let guard = match CONFIG_SERVER_CLIENT.lock() {
Ok(guard) => guard,
Err(err) => {
set_error_msg(&format!("failed to lock config server client: {}", err));
@@ -528,29 +439,25 @@ pub(crate) fn stop_config_server_client() -> c_int {
return -1;
}
let hooks = managed.hooks.clone();
let managed = guard.take().expect("config server client exists");
// Keep the client discoverable until the canonical transaction drains its
// tracking. Earlier removals must still retire IDs from these same hooks.
drop(guard);
let _remote_mutation_guard = lock_remote_instance_mutation();
let tracked_ids = hooks.start_stopping();
drop(managed);
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
Ok(guard) => guard,
let delete_result = ffi_context().runtime.block_on(
ffi_context()
.process_management
.delete_owned_network_instances_selected_by(|| hooks.start_stopping()),
);
let managed = match CONFIG_SERVER_CLIENT.lock() {
Ok(mut guard) => guard.take(),
Err(err) => {
hooks.wait_for_callback_delivery();
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
set_error_msg(&format!("failed to lock instance mutation: {}", err));
set_error_msg(&format!("failed to lock config server client: {err}"));
return -1;
}
};
let delete_result = INSTANCE_MANAGER.delete_network_instance(tracked_ids.clone());
if delete_result.is_ok() {
remove_instance_name_ids(&tracked_ids);
remove_data_plane_handles_by_instance_ids(&tracked_ids);
}
drop(_mutation_guard);
drop(managed);
hooks.wait_for_callback_delivery();
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
@@ -1,928 +0,0 @@
#[cfg(feature = "ffi-dataplane")]
use std::{
future::Future,
net::{IpAddr, SocketAddr},
sync::{
Arc, RwLock,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
#[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, ReadHalf, WriteHalf};
#[cfg(feature = "ffi-dataplane")]
use tokio_util::sync::CancellationToken;
#[cfg(feature = "ffi-dataplane")]
use uuid::Uuid;
#[cfg(feature = "ffi-dataplane")]
use crate::{
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
error::{free_string, set_error_msg},
state::{INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP},
};
#[cfg(feature = "ffi-dataplane")]
static NEXT_DATA_PLANE_HANDLE: AtomicU64 = AtomicU64::new(1);
#[cfg(feature = "ffi-dataplane")]
static DATA_PLANE_HANDLES: once_cell::sync::Lazy<DashMap<u64, DataPlaneHandle>> =
once_cell::sync::Lazy::new(DashMap::new);
#[cfg(feature = "ffi-dataplane")]
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
once_cell::sync::Lazy::new(|| RwLock::new(()));
#[cfg(feature = "ffi-dataplane")]
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.
pub(crate) close_token: CancellationToken,
pub(crate) resource: DataPlaneResource,
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) struct TcpHalves {
pub(crate) read: tokio::sync::Mutex<ReadHalf<DataPlaneTcpStream>>,
pub(crate) write: tokio::sync::Mutex<WriteHalf<DataPlaneTcpStream>>,
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) enum DataPlaneResource {
Tcp(Arc<TcpHalves>),
TcpListener(Arc<tokio::sync::Mutex<DataPlaneTcpListener>>),
Udp(Arc<DataPlaneUdpSocket>),
}
// Several helper functions for FFI data plane operations to facilitate logic reuse.
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn next_handle() -> u64 {
NEXT_DATA_PLANE_HANDLE.fetch_add(1, Ordering::Relaxed)
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn timeout_duration(timeout_ms: u64) -> Duration {
Duration::from_millis(timeout_ms)
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<String> {
if ptr.is_null() {
set_error_msg(&format!("{} is null", name));
return None;
}
Some(
unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned(),
)
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn get_instance_id(inst_name: &str) -> Option<uuid::Uuid> {
INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id.value())
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
let ip = match host.parse::<IpAddr>() {
Ok(ip) => ip,
Err(e) => {
set_error_msg(&format!("failed to parse ip address: {}", e));
return None;
}
};
Some(SocketAddr::new(ip, port))
}
/// 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")]
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) => {
set_error_msg(&format!("failed to encode ip: {}", e));
None
}
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn get_runtime_handle(
inst_id: &uuid::Uuid,
deadline: std::time::Instant,
) -> Option<tokio::runtime::Handle> {
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
let Some(rt) = INSTANCE_MANAGER.data_plane_wait_runtime_handle(inst_id, remaining) else {
set_error_msg("instance runtime is not ready");
return None;
};
Some(rt)
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn insert_tcp_stream_handle(
instance_id: uuid::Uuid,
runtime: tokio::runtime::Handle,
stream: DataPlaneTcpStream,
) -> u64 {
let (rd, wr) = tokio::io::split(stream);
let handle = next_handle();
DATA_PLANE_HANDLES.insert(
handle,
DataPlaneHandle {
instance_id,
runtime,
close_token: CancellationToken::new(),
resource: DataPlaneResource::Tcp(Arc::new(TcpHalves {
read: tokio::sync::Mutex::new(rd),
write: tokio::sync::Mutex::new(wr),
})),
},
);
handle
}
#[cfg(feature = "ffi-dataplane")]
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<TcpHalves>, 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<TcpHalves>,
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(),
h.instance_id,
)),
DataPlaneResource::TcpListener(_) | DataPlaneResource::Udp(_) => {
set_error_msg("handle is not a tcp stream");
None
}
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn get_tcp_listener(
handle: u64,
) -> Option<(
Arc<tokio::sync::Mutex<DataPlaneTcpListener>>,
tokio::runtime::Handle,
CancellationToken,
uuid::Uuid,
)> {
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
set_error_msg("tcp listener handle not found");
return None;
};
match &h.resource {
DataPlaneResource::TcpListener(listener) => Some((
listener.clone(),
h.runtime.clone(),
h.close_token.clone(),
h.instance_id,
)),
DataPlaneResource::Tcp(_) | DataPlaneResource::Udp(_) => {
set_error_msg("handle is not a tcp listener");
None
}
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn get_udp_socket(
handle: u64,
) -> Option<(
Arc<DataPlaneUdpSocket>,
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<DataPlaneUdpSocket>,
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(),
h.instance_id,
)),
DataPlaneResource::Tcp(_) | DataPlaneResource::TcpListener(_) => {
set_error_msg("handle is not a udp socket");
None
}
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) {
if ids.is_empty() {
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();
false
} else {
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")]
pub(crate) fn data_plane_rejected() -> bool {
if in_config_server_callback() {
set_error_msg("cannot use data plane from config server callback");
true
} else if is_config_server_active_or_stopping() {
set_error_msg("cannot use data plane while config server client is active");
true
} else {
false
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn enter_data_plane_operation() -> Option<std::sync::RwLockReadGuard<'static, ()>> {
if data_plane_rejected() {
return None;
}
let guard = match DATA_PLANE_USAGE_LOCK.read() {
Ok(guard) => guard,
Err(err) => {
set_error_msg(&format!("failed to lock data plane usage: {}", err));
return None;
}
};
if data_plane_rejected() {
return None;
}
Some(guard)
}
/// Run an IO op on the resource's owning runtime, supporting
/// timeout and cancellation.
#[cfg(feature = "ffi-dataplane")]
async fn run_with_cancel<T, F>(
close_token: &CancellationToken,
timeout_ms: u64,
error_prefix: &str,
op: F,
) -> Option<Result<T, std::io::Error>>
where
F: Future<Output = Result<T, std::io::Error>>,
{
tokio::select! {
biased;
_ = close_token.cancelled() => {
set_error_msg(&format!("{}: handle closed", error_prefix));
None
}
res = tokio::time::timeout(timeout_duration(timeout_ms), op) => match res {
Ok(r) => Some(r),
Err(_) => {
set_error_msg(&format!("{} timed out", error_prefix));
None
}
}
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn lock_for_config_server_start()
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
let guard = DATA_PLANE_USAGE_LOCK
.write()
.map_err(|err| format!("failed to lock data plane usage: {}", err))?;
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)
}
/// # Safety
/// Open a TCP stream through an EasyTier instance data plane. Returns 0 on
/// failure. On success, writes the local socket address chosen for this
/// connection into `out_local_ip` (a heap-allocated C string the caller must
/// release via `free_string`) and `out_local_port`. Both out pointers must be
/// non-null.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_tcp_connect(
inst_name: *const std::ffi::c_char,
dst_ip: *const std::ffi::c_char,
dst_port: std::ffi::c_ushort,
timeout_ms: u64,
out_local_ip: *mut *const std::ffi::c_char,
out_local_port: *mut std::ffi::c_ushort,
) -> u64 {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return 0,
};
if out_local_ip.is_null() || out_local_port.is_null() {
set_error_msg("output pointer is null");
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(inst_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 deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
return 0;
};
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
let result =
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_connect(&inst_id, dst_addr, remaining));
match result {
Ok(stream) => {
let local_addr = stream.local_addr();
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
return 0;
};
let handle = insert_tcp_stream_handle(inst_id, runtime, stream);
unsafe {
*out_local_ip = local_ip as *const std::ffi::c_char;
*out_local_port = local_addr.port();
}
handle
}
Err(e) => {
set_error_msg(&format!("failed to connect tcp data plane: {}", e));
0
}
}
}
/// # Safety
/// Bind a TCP listener through an EasyTier instance data plane. Returns 0 on
/// failure. The local address actually bound is written into `out_local_ip` /
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_tcp_bind(
inst_name: *const std::ffi::c_char,
local_port: std::ffi::c_ushort,
timeout_ms: u64,
out_local_ip: *mut *const std::ffi::c_char,
out_local_port: *mut std::ffi::c_ushort,
) -> u64 {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return 0,
};
if out_local_ip.is_null() || out_local_port.is_null() {
set_error_msg("output pointer is null");
return 0;
}
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
return 0;
};
let Some(inst_id) = get_instance_id(&inst_name) else {
set_error_msg("instance not found");
return 0;
};
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
return 0;
};
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
let result =
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_bind(&inst_id, local_port, remaining));
match result {
Ok(listener) => {
let local_addr = listener.local_addr();
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
return 0;
};
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();
}
handle
}
Err(e) => {
set_error_msg(&format!("failed to bind tcp data plane: {}", e));
0
}
}
}
/// # Safety
/// Accept one connection from a TCP data-plane listener. Returns a TCP stream
/// handle, or 0 on failure. Local and peer addresses are written into out
/// parameters; returned IP strings must be released via `free_string`.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_tcp_accept(
handle: u64,
timeout_ms: 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 {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return 0,
};
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 Some((listener, runtime, close_token, instance_id)) = get_tcp_listener(handle) else {
return 0;
};
let ret = runtime.block_on(async move {
let mut listener = listener.lock().await;
run_with_cancel(
&close_token,
timeout_ms,
"tcp data plane accept",
listener.accept(),
)
.await
});
match ret {
Some(Ok((stream, peer_addr))) => {
let local_addr = stream.local_addr();
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
return 0;
};
let Some(peer_ip) = into_ffi_ip_cstring(peer_addr.ip()) else {
free_string(local_ip);
return 0;
};
let stream_handle = insert_tcp_stream_handle(instance_id, runtime, stream);
unsafe {
*out_local_ip = local_ip as *const std::ffi::c_char;
*out_local_port = local_addr.port();
*out_peer_ip = peer_ip as *const std::ffi::c_char;
*out_peer_port = peer_addr.port();
}
stream_handle
}
Some(Err(e)) => {
set_error_msg(&format!("failed to accept tcp data plane: {}", e));
0
}
None => 0,
}
}
/// # Safety
/// Read from a TCP data-plane stream.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_tcp_read(
handle: u64,
buf: *mut std::ffi::c_uchar,
len: u32,
timeout_ms: u64,
) -> std::ffi::c_int {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return -1,
};
if buf.is_null() {
set_error_msg("buf is null");
return -1;
}
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
return -1;
};
// Safety: caller-owned buffer outlives this blocking call.
let buf = unsafe { std::slice::from_raw_parts_mut(buf, len as usize) };
runtime.block_on(async move {
let mut rd = halves.read.lock().await;
match run_with_cancel(
&close_token,
timeout_ms,
"failed to read tcp data plane",
rd.read(buf),
)
.await
{
Some(Ok(n)) => n as std::ffi::c_int,
Some(Err(e)) => {
set_error_msg(&format!("failed to read tcp data plane: {}", e));
-1
}
None => -1,
}
})
}
/// # Safety
/// Write to a TCP data-plane stream.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_tcp_write(
handle: u64,
buf: *const std::ffi::c_uchar,
len: u32,
timeout_ms: u64,
) -> std::ffi::c_int {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return -1,
};
if buf.is_null() {
set_error_msg("buf is null");
return -1;
}
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
return -1;
};
let total = len as usize;
// Safety: caller-owned buffer outlives this blocking call.
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
runtime.block_on(async move {
let mut wr = halves.write.lock().await;
// Use `write_all` to honor `net.Conn::Write` semantics on the Go side
// (must write everything or return an error); single `write()` can
// silently short-write and corrupt streams that the caller assumes are
// fully written.
match run_with_cancel(
&close_token,
timeout_ms,
"failed to write tcp data plane",
wr.write_all(buf),
)
.await
{
Some(Ok(())) => total as std::ffi::c_int,
Some(Err(e)) => {
set_error_msg(&format!("failed to write tcp data plane: {}", e));
-1
}
None => -1,
}
})
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn data_plane_tcp_close(handle: u64) -> std::ffi::c_int {
let _data_plane_usage_guard = match enter_data_plane_operation() {
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 {
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
"handle is not a tcp stream"
} else {
"tcp stream handle not found"
});
return -1;
};
h.close_token.cancel();
if let DataPlaneResource::Tcp(halves) = h.resource {
// Best-effort half-close; if write half is in use, the in-flight call
// observes the cancel token and releases the lock shortly after.
h.runtime.spawn(async move {
if let Ok(mut wr) = halves.write.try_lock() {
let _ = wr.shutdown().await;
}
});
}
0
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn data_plane_tcp_listener_close(handle: u64) -> std::ffi::c_int {
let _data_plane_usage_guard = match enter_data_plane_operation() {
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 {
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
"handle is not a tcp listener"
} else {
"tcp listener handle not found"
});
return -1;
};
h.close_token.cancel();
0
}
/// # Safety
/// Bind a UDP socket through an EasyTier instance data plane. Returns 0 on
/// failure. The local address actually bound (which may differ from the
/// requested port when `local_port == 0`) is written into `out_local_ip` /
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_udp_bind(
inst_name: *const std::ffi::c_char,
local_port: std::ffi::c_ushort,
timeout_ms: u64,
out_local_ip: *mut *const std::ffi::c_char,
out_local_port: *mut std::ffi::c_ushort,
) -> u64 {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return 0,
};
if out_local_ip.is_null() || out_local_port.is_null() {
set_error_msg("output pointer is null");
return 0;
}
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
return 0;
};
let Some(inst_id) = get_instance_id(&inst_name) else {
set_error_msg("instance not found");
return 0;
};
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
return 0;
};
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
let result =
runtime.block_on(INSTANCE_MANAGER.data_plane_udp_bind(&inst_id, local_port, remaining));
match result {
Ok(socket) => {
let local_addr = socket.local_addr();
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
return 0;
};
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();
}
handle
}
Err(e) => {
set_error_msg(&format!("failed to bind udp data plane: {}", e));
0
}
}
}
/// # Safety
/// Send a datagram through a UDP data-plane socket.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_udp_send_to(
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,
) -> std::ffi::c_int {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return -1,
};
if buf.is_null() {
set_error_msg("buf is null");
return -1;
}
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
return -1;
};
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
return -1;
};
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
return -1;
};
let total = len as usize;
// Safety: caller-owned buffer outlives this blocking call.
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
runtime.block_on(async move {
match run_with_cancel(
&close_token,
timeout_ms,
"failed to send udp data plane",
socket.send_to(buf, dst_addr),
)
.await
{
Some(Ok(n)) => n as std::ffi::c_int,
Some(Err(e)) => {
set_error_msg(&format!("failed to send udp data plane: {}", e));
-1
}
None => -1,
}
})
}
/// # Safety
/// Receive a datagram from a UDP data-plane socket.
#[cfg(feature = "ffi-dataplane")]
pub(crate) unsafe fn data_plane_udp_recv_from(
handle: u64,
buf: *mut std::ffi::c_uchar,
len: u32,
out_ip: *mut *const std::ffi::c_char,
out_port: *mut std::ffi::c_ushort,
timeout_ms: u64,
) -> std::ffi::c_int {
let _data_plane_usage_guard = match enter_data_plane_operation() {
Some(guard) => guard,
None => return -1,
};
if buf.is_null() || out_ip.is_null() || out_port.is_null() {
set_error_msg("output pointer is null");
return -1;
}
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
return -1;
};
let total = len as usize;
// Safety: caller-owned buffer outlives this blocking call.
let buf = unsafe { std::slice::from_raw_parts_mut(buf, total) };
let ret = runtime.block_on(run_with_cancel(
&close_token,
timeout_ms,
"udp data plane receive",
socket.recv_from(buf),
));
match ret {
Some(Ok((n, addr))) => {
// The returned ip pointer must be released by the caller via
// `free_string` (which calls `CString::from_raw`, matching
// `CString::into_raw` here).
let Some(ip_cstr) = into_ffi_ip_cstring(addr.ip()) else {
return -1;
};
unsafe {
*out_ip = ip_cstr as *const std::ffi::c_char;
*out_port = addr.port() as std::ffi::c_ushort;
}
n as std::ffi::c_int
}
Some(Err(e)) => {
set_error_msg(&format!("failed to receive udp data plane: {}", e));
-1
}
None => -1,
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn data_plane_udp_close(handle: u64) -> std::ffi::c_int {
let _data_plane_usage_guard = match enter_data_plane_operation() {
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 {
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
"handle is not a udp socket"
} else {
"udp socket handle not found"
});
return -1;
};
h.close_token.cancel();
0
}
#[cfg(all(test, feature = "ffi-dataplane"))]
mod tests {
use super::*;
use std::{sync::mpsc, time::Duration};
#[test]
fn config_server_start_waits_for_data_plane_operation() {
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
let (done_tx, done_rx) = mpsc::channel();
let waiter = std::thread::spawn(move || {
let _write_guard = lock_for_config_server_start().unwrap();
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();
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();
}
}
@@ -0,0 +1,685 @@
use std::{
ffi::{c_char, c_int, c_uchar},
net::{IpAddr, Ipv4Addr, SocketAddr},
ptr,
};
use easytier_core::gateway::DataPlaneErrorKind;
use super::session::{self, NativeDataPlaneError, NativeDataPlaneResult};
use crate::{
error::set_error_msg,
strings::c_str_to_string,
types::{DataPlaneCompletion, DataPlaneSocketAddr},
};
pub const DATA_PLANE_DEADLINE_READ: u32 = 1 << 0;
pub const DATA_PLANE_DEADLINE_WRITE: u32 = 1 << 1;
fn failure(error: NativeDataPlaneError) -> c_int {
set_error_msg(&error.message);
-(error.kind as c_int)
}
fn status(result: NativeDataPlaneResult<()>) -> c_int {
match result {
Ok(()) => 0,
Err(error) => failure(error),
}
}
fn invalid(message: impl Into<String>) -> NativeDataPlaneError {
NativeDataPlaneError {
kind: DataPlaneErrorKind::Io,
message: message.into(),
}
}
fn socket_addr(address: DataPlaneSocketAddr) -> NativeDataPlaneResult<SocketAddr> {
let ip = match address.family {
4 => IpAddr::V4(Ipv4Addr::new(
address.address[0],
address.address[1],
address.address[2],
address.address[3],
)),
6 => {
return Err(NativeDataPlaneError {
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
message: "IPv6 is not supported by data-plane ABI v3".to_string(),
});
}
family => {
return Err(NativeDataPlaneError {
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
message: format!("unsupported address family {family}"),
});
}
};
Ok(SocketAddr::new(ip, address.port))
}
fn ffi_socket_addr(address: SocketAddr) -> DataPlaneSocketAddr {
match address.ip() {
IpAddr::V4(ip) => {
let mut bytes = [0; 16];
bytes[..4].copy_from_slice(&ip.octets());
DataPlaneSocketAddr {
family: 4,
port: address.port(),
address: bytes,
}
}
IpAddr::V6(ip) => DataPlaneSocketAddr {
family: 6,
port: address.port(),
address: ip.octets(),
},
}
}
unsafe fn copy_input(ptr: *const c_uchar, len: u32) -> NativeDataPlaneResult<Vec<u8>> {
if len == 0 {
return Ok(Vec::new());
}
if ptr.is_null() {
return Err(invalid("input buffer is null"));
}
Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec())
}
unsafe fn output_slice<'a>(ptr: *mut c_uchar, len: u32) -> NativeDataPlaneResult<&'a mut [u8]> {
if len == 0 {
return Ok(&mut []);
}
if ptr.is_null() {
return Err(invalid("output buffer is null"));
}
Ok(unsafe { std::slice::from_raw_parts_mut(ptr, len as usize) })
}
fn write_operation(
out_operation: *mut u64,
submit: impl FnOnce() -> NativeDataPlaneResult<u64>,
) -> c_int {
if out_operation.is_null() {
return failure(invalid("out_operation is null"));
}
match submit() {
Ok(operation) => {
unsafe {
*out_operation = operation;
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// If non-null, `inst_name` must point to a valid NUL-terminated string.
/// `out_session` must be null or point to writable, properly aligned storage
/// for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_session_open(
inst_name: *const c_char,
out_session: *mut u64,
) -> c_int {
if out_session.is_null() {
return failure(invalid("out_session is null"));
}
unsafe {
*out_session = 0;
}
let inst_name = match unsafe { c_str_to_string(inst_name, "inst_name") } {
Ok(inst_name) => inst_name,
Err(error) => return failure(invalid(error)),
};
match session::open(&inst_name) {
Ok(handle) => {
unsafe {
*out_session = handle;
}
0
}
Err(error) => failure(error),
}
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_session_close(session: u64) -> c_int {
status(super::session::close(session))
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_connect_submit(
session: u64,
peer_addr: DataPlaneSocketAddr,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
let peer_addr = match socket_addr(peer_addr) {
Ok(address) => address,
Err(error) => return failure(error),
};
write_operation(out_operation, || {
super::session::submit_tcp_connect(session, peer_addr, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_bind_submit(
session: u64,
local_port: u16,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_tcp_bind(session, local_port, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_accept_submit(
session: u64,
listener: u64,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_tcp_accept(session, listener, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_read_submit(
session: u64,
stream: u64,
max_len: u32,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_tcp_read(session, stream, max_len)
})
}
/// # Safety
///
/// When `len` is nonzero, `data` must point to `len` readable bytes.
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_write_submit(
session: u64,
stream: u64,
data: *const c_uchar,
len: u32,
out_operation: *mut u64,
) -> c_int {
let data = match unsafe { copy_input(data, len) } {
Ok(data) => data,
Err(error) => return failure(error),
};
write_operation(out_operation, || {
super::session::submit_tcp_write(session, stream, data)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_bind_submit(
session: u64,
local_port: u16,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_udp_bind(session, local_port, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_receive_submit(
session: u64,
socket: u64,
max_len: u32,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_udp_receive(session, socket, max_len)
})
}
/// # Safety
///
/// When `len` is nonzero, `data` must point to `len` readable bytes.
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_send_submit(
session: u64,
socket: u64,
peer_addr: DataPlaneSocketAddr,
data: *const c_uchar,
len: u32,
out_operation: *mut u64,
) -> c_int {
let peer_addr = match socket_addr(peer_addr) {
Ok(address) => address,
Err(error) => return failure(error),
};
let data = match unsafe { copy_input(data, len) } {
Ok(data) => data,
Err(error) => return failure(error),
};
write_operation(out_operation, || {
super::session::submit_udp_send(session, socket, peer_addr, data)
})
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_resource_deadline_set(
session: u64,
resource: u64,
direction: u32,
timeout_ms: u64,
) -> c_int {
let read = direction & DATA_PLANE_DEADLINE_READ != 0;
let write = direction & DATA_PLANE_DEADLINE_WRITE != 0;
if direction == 0 || direction & !(DATA_PLANE_DEADLINE_READ | DATA_PLANE_DEADLINE_WRITE) != 0 {
return failure(invalid(format!("invalid deadline direction {direction}")));
}
status(super::session::set_resource_deadline(
session, resource, read, write, timeout_ms,
))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_operation_cancel(session: u64, operation: u64) -> c_int {
status(super::session::cancel_operation(session, operation))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_operation_free(session: u64, operation: u64) -> c_int {
status(super::session::free_operation(session, operation))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_resource_close(session: u64, resource: u64) -> c_int {
status(super::session::close_resource(session, resource))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_completion_wait(session: u64, timeout_ms: u64) -> c_int {
match super::session::completion_wait(session, timeout_ms) {
Ok(true) => 1,
Ok(false) => 0,
Err(error) => failure(error),
}
}
/// # Safety
///
/// When `capacity` is nonzero, `completions` must point to writable, properly
/// aligned storage for `capacity` consecutive [`DataPlaneCompletion`] values.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_completion_drain(
session: u64,
completions: *mut DataPlaneCompletion,
capacity: u32,
) -> c_int {
if capacity != 0 && completions.is_null() {
return failure(invalid("completions is null"));
}
let drained = match super::session::drain_completions(session, capacity as usize) {
Ok(drained) => drained,
Err(error) => return failure(error),
};
for (index, completion) in drained.iter().enumerate() {
unsafe {
ptr::write(
completions.add(index),
DataPlaneCompletion {
operation_id: completion.operation_id.get(),
operation_kind: completion.kind as u16,
status: completion.status.code(),
},
);
}
}
drained.len() as c_int
}
/// # Safety
///
/// `out_size` must be null or point to writable, properly aligned storage for
/// one `u32`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_result_size(
session: u64,
operation: u64,
out_size: *mut u32,
) -> c_int {
if out_size.is_null() {
return failure(invalid("out_size is null"));
}
match super::session::result_size(session, operation) {
Ok(size) => match u32::try_from(size) {
Ok(size) => {
unsafe {
*out_size = size;
}
0
}
Err(_) => failure(invalid("data-plane result size exceeds u32")),
},
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_connect_result_take(
session: u64,
operation: u64,
out_stream: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
out_peer_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
return failure(invalid("TCP connect result output pointer is null"));
}
match super::session::take_tcp_connect(session, operation) {
Ok(result) => {
unsafe {
*out_stream = result.stream;
*out_local_addr = ffi_socket_addr(result.local_addr);
*out_peer_addr = ffi_socket_addr(result.peer_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_bind_result_take(
session: u64,
operation: u64,
out_listener: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_listener.is_null() || out_local_addr.is_null() {
return failure(invalid("TCP bind result output pointer is null"));
}
match super::session::take_tcp_bind(session, operation) {
Ok(result) => {
unsafe {
*out_listener = result.listener;
*out_local_addr = ffi_socket_addr(result.local_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_accept_result_take(
session: u64,
operation: u64,
out_stream: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
out_peer_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
return failure(invalid("TCP accept result output pointer is null"));
}
match super::session::take_tcp_accept(session, operation) {
Ok(result) => {
unsafe {
*out_stream = result.stream;
*out_local_addr = ffi_socket_addr(result.local_addr);
*out_peer_addr = ffi_socket_addr(result.peer_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
/// Each scalar output pointer must be null or point to writable, properly
/// aligned storage for its pointee type. Non-null output ranges must not
/// overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_read_result_take(
session: u64,
operation: u64,
data: *mut c_uchar,
capacity: u32,
out_len: *mut u32,
out_eof: *mut bool,
) -> c_int {
if out_len.is_null() || out_eof.is_null() {
return failure(invalid("TCP read result output pointer is null"));
}
let data = match unsafe { output_slice(data, capacity) } {
Ok(data) => data,
Err(error) => return failure(error),
};
match super::session::take_tcp_read(session, operation, data) {
Ok(result) => {
unsafe {
*out_len = result.len as u32;
*out_eof = result.eof;
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// `out_len` must be null or point to writable, properly aligned storage for
/// one `u32`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_write_result_take(
session: u64,
operation: u64,
out_len: *mut u32,
) -> c_int {
if out_len.is_null() {
return failure(invalid("out_len is null"));
}
match super::session::take_tcp_write(session, operation) {
Ok(len) => match u32::try_from(len) {
Ok(len) => {
unsafe {
*out_len = len;
}
0
}
Err(_) => failure(invalid("TCP write result exceeds u32")),
},
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_bind_result_take(
session: u64,
operation: u64,
out_socket: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_socket.is_null() || out_local_addr.is_null() {
return failure(invalid("UDP bind result output pointer is null"));
}
match super::session::take_udp_bind(session, operation) {
Ok(result) => {
unsafe {
*out_socket = result.socket;
*out_local_addr = ffi_socket_addr(result.local_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
/// Each scalar output pointer must be null or point to writable, properly
/// aligned storage for its pointee type. Non-null output ranges must not
/// overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_receive_result_take(
session: u64,
operation: u64,
data: *mut c_uchar,
capacity: u32,
out_len: *mut u32,
out_peer_addr: *mut DataPlaneSocketAddr,
out_truncated: *mut bool,
) -> c_int {
if out_len.is_null() || out_peer_addr.is_null() || out_truncated.is_null() {
return failure(invalid("UDP receive result output pointer is null"));
}
let data = match unsafe { output_slice(data, capacity) } {
Ok(data) => data,
Err(error) => return failure(error),
};
match super::session::take_udp_receive(session, operation, data) {
Ok(result) => {
unsafe {
*out_len = result.len as u32;
*out_peer_addr = ffi_socket_addr(result.peer_addr);
*out_truncated = result.truncated;
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// `out_len` must be null or point to writable, properly aligned storage for
/// one `u32`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_send_result_take(
session: u64,
operation: u64,
out_len: *mut u32,
) -> c_int {
if out_len.is_null() {
return failure(invalid("out_len is null"));
}
match super::session::take_udp_send(session, operation) {
Ok(len) => match u32::try_from(len) {
Ok(len) => {
unsafe {
*out_len = len;
}
0
}
Err(_) => failure(invalid("UDP send result exceeds u32")),
},
Err(error) => failure(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn socket_address_round_trip() {
let address = "127.0.0.1:1234".parse::<SocketAddr>().unwrap();
assert_eq!(socket_addr(ffi_socket_addr(address)).unwrap(), address);
}
#[test]
fn ipv6_is_rejected_by_v3() {
let error = socket_addr(ffi_socket_addr(
"[2001:db8::1]:4321".parse::<SocketAddr>().unwrap(),
))
.unwrap_err();
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
}
#[test]
fn invalid_address_family_is_stable() {
let error = socket_addr(DataPlaneSocketAddr {
family: 9,
..Default::default()
})
.unwrap_err();
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
}
#[test]
fn invalid_deadline_direction_is_rejected_before_session_lookup() {
let invalid = -(DataPlaneErrorKind::Io as c_int);
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 0, 0), invalid);
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 4, 0), invalid);
}
#[test]
fn null_operation_output_does_not_submit() {
let submitted = std::cell::Cell::new(false);
assert_eq!(
write_operation(std::ptr::null_mut(), || {
submitted.set(true);
Ok(1)
}),
-(DataPlaneErrorKind::Io as c_int)
);
assert!(!submitted.get());
}
}
@@ -0,0 +1,16 @@
//! Native C ABI adapter for the instance-scoped data-plane operation broker.
#[cfg(feature = "ffi-dataplane")]
mod abi;
#[cfg(feature = "ffi-dataplane")]
mod session;
#[cfg(feature = "ffi-dataplane")]
pub use abi::*;
#[cfg(feature = "ffi-dataplane")]
pub(crate) use session::{
lock_for_config_server_start, remove_data_plane_sessions_by_instance_ids,
};
#[cfg(not(feature = "ffi-dataplane"))]
pub(crate) fn remove_data_plane_sessions_by_instance_ids(_ids: &[uuid::Uuid]) {}
@@ -0,0 +1,646 @@
use std::{
collections::HashMap,
net::SocketAddr,
sync::{
Arc, Mutex, RwLock,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Duration,
};
use easytier::instance::host::NativeInstanceHost;
use easytier_core::gateway::{
DataPlaneCompletionDescriptor, DataPlaneError, DataPlaneErrorKind, DataPlaneOperationId,
DataPlaneOperationKind, DataPlaneOperationResult, DataPlaneResourceId, DataPlaneSession,
};
use uuid::Uuid;
use crate::{
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
state::{ffi_context, resolve_instance_id_by_name},
};
type CoreDataPlaneSession = DataPlaneSession<NativeInstanceHost>;
static NEXT_SESSION_HANDLE: AtomicU64 = AtomicU64::new(1);
static SESSIONS: once_cell::sync::Lazy<Mutex<HashMap<u64, Arc<NativeDataPlaneSession>>>> =
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
once_cell::sync::Lazy::new(|| RwLock::new(()));
#[derive(Debug)]
pub(super) struct NativeDataPlaneError {
pub(super) kind: DataPlaneErrorKind,
pub(super) message: String,
}
impl NativeDataPlaneError {
fn new(kind: DataPlaneErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
fn invalid(message: impl Into<String>) -> Self {
Self::new(DataPlaneErrorKind::Io, message)
}
fn closed(message: impl Into<String>) -> Self {
Self::new(DataPlaneErrorKind::HandleClosed, message)
}
}
impl From<DataPlaneError> for NativeDataPlaneError {
fn from(error: DataPlaneError) -> Self {
Self::new(error.kind(), error.message())
}
}
pub(super) type NativeDataPlaneResult<T> = Result<T, NativeDataPlaneError>;
pub(super) struct TcpConnectResult {
pub(super) stream: u64,
pub(super) local_addr: SocketAddr,
pub(super) peer_addr: SocketAddr,
}
pub(super) struct TcpBindResult {
pub(super) listener: u64,
pub(super) local_addr: SocketAddr,
}
pub(super) struct TcpAcceptResult {
pub(super) stream: u64,
pub(super) local_addr: SocketAddr,
pub(super) peer_addr: SocketAddr,
}
pub(super) struct TcpReadResult {
pub(super) len: usize,
pub(super) eof: bool,
}
pub(super) struct UdpBindResult {
pub(super) socket: u64,
pub(super) local_addr: SocketAddr,
}
pub(super) struct UdpReceiveResult {
pub(super) len: usize,
pub(super) peer_addr: SocketAddr,
pub(super) truncated: bool,
}
struct NativeDataPlaneSession {
instance_id: Uuid,
runtime: tokio::runtime::Handle,
core: Arc<CoreDataPlaneSession>,
submit_gate: Mutex<()>,
closed: AtomicBool,
}
impl NativeDataPlaneSession {
fn close(&self) {
let _gate = self
.submit_gate
.lock()
.unwrap_or_else(|error| error.into_inner());
if self.closed.swap(true, Ordering::AcqRel) {
return;
}
self.core.discard_all();
}
fn call<T>(
&self,
call: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<T, DataPlaneError>,
) -> NativeDataPlaneResult<T> {
let _gate = self
.submit_gate
.lock()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
if self.closed.load(Ordering::Acquire) {
return Err(NativeDataPlaneError::closed(
"native data-plane session is closed",
));
}
let _runtime = self.runtime.enter();
call(&self.core).map_err(Into::into)
}
fn submit(
&self,
submit: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<DataPlaneOperationId, DataPlaneError>,
) -> NativeDataPlaneResult<u64> {
self.call(submit).map(DataPlaneOperationId::get)
}
}
fn sessions()
-> NativeDataPlaneResult<std::sync::MutexGuard<'static, HashMap<u64, Arc<NativeDataPlaneSession>>>>
{
SESSIONS
.lock()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))
}
fn get_session(handle: u64) -> NativeDataPlaneResult<Arc<NativeDataPlaneSession>> {
if handle == 0 {
return Err(NativeDataPlaneError::closed(
"native data-plane session handle is invalid",
));
}
let session = sessions()?
.get(&handle)
.cloned()
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
if session.closed.load(Ordering::Acquire) {
return Err(NativeDataPlaneError::closed(
"native data-plane session is closed",
));
}
Ok(session)
}
fn next_session_handle(
sessions: &HashMap<u64, Arc<NativeDataPlaneSession>>,
) -> NativeDataPlaneResult<u64> {
for _ in 0..sessions.len().saturating_add(2) {
let handle = NEXT_SESSION_HANDLE.fetch_add(1, Ordering::Relaxed);
if handle != 0 && !sessions.contains_key(&handle) {
return Ok(handle);
}
}
Err(NativeDataPlaneError::new(
DataPlaneErrorKind::ResourceLimit,
"native data-plane session handle space is exhausted",
))
}
fn reject_data_plane_use() -> NativeDataPlaneResult<()> {
if in_config_server_callback() {
Err(NativeDataPlaneError::invalid(
"cannot use data plane from config server callback",
))
} else if is_config_server_active_or_stopping() {
Err(NativeDataPlaneError::invalid(
"cannot use data plane while config server client is active",
))
} else {
Ok(())
}
}
pub(super) fn open(inst_name: &str) -> NativeDataPlaneResult<u64> {
reject_data_plane_use()?;
let _usage = DATA_PLANE_USAGE_LOCK
.read()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
reject_data_plane_use()?;
let instance_id = resolve_instance_id_by_name(inst_name)
.map_err(NativeDataPlaneError::invalid)?
.ok_or_else(|| NativeDataPlaneError::closed("instance not found"))?;
let manager = &ffi_context().manager;
let core = manager.data_plane_session(&instance_id).ok_or_else(|| {
NativeDataPlaneError::closed("instance data-plane session is unavailable")
})?;
let runtime = manager
.data_plane_runtime_handle(&instance_id)
.ok_or_else(|| NativeDataPlaneError::closed("instance runtime is unavailable"))?;
let mut sessions = sessions()?;
if sessions
.values()
.any(|session| session.instance_id == instance_id)
{
return Err(NativeDataPlaneError::new(
DataPlaneErrorKind::ResourceLimit,
"instance already has an open native data-plane session",
));
}
let handle = next_session_handle(&sessions)?;
sessions.insert(
handle,
Arc::new(NativeDataPlaneSession {
instance_id,
runtime,
core,
submit_gate: Mutex::new(()),
closed: AtomicBool::new(false),
}),
);
Ok(handle)
}
pub(super) fn close(handle: u64) -> NativeDataPlaneResult<()> {
let _usage = DATA_PLANE_USAGE_LOCK
.read()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
let mut sessions = sessions()?;
let session = sessions
.remove(&handle)
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
// Keep the registry locked until the shared core namespace is empty. An
// open for the same instance must not publish a replacement session before
// this old wrapper finishes discarding its operations and resources.
session.close();
Ok(())
}
fn timeout(timeout_ms: u64) -> Option<Duration> {
(timeout_ms != u64::MAX).then(|| Duration::from_millis(timeout_ms))
}
fn operation_id(raw: u64) -> NativeDataPlaneResult<DataPlaneOperationId> {
DataPlaneOperationId::from_raw(raw)
.ok_or_else(|| NativeDataPlaneError::closed("data-plane operation handle is invalid"))
}
fn resource_id(raw: u64) -> NativeDataPlaneResult<DataPlaneResourceId> {
DataPlaneResourceId::from_raw(raw)
.ok_or_else(|| NativeDataPlaneError::closed("data-plane resource handle is invalid"))
}
pub(super) fn submit_tcp_connect(
session: u64,
peer_addr: SocketAddr,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
get_session(session)?.submit(|core| core.submit_tcp_connect(peer_addr, timeout(timeout_ms)))
}
pub(super) fn submit_tcp_bind(
session: u64,
local_port: u16,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
get_session(session)?.submit(|core| core.submit_tcp_bind(local_port, timeout(timeout_ms)))
}
pub(super) fn submit_tcp_accept(
session: u64,
listener: u64,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
let listener = resource_id(listener)?;
get_session(session)?.submit(|core| core.submit_tcp_accept(listener, timeout(timeout_ms)))
}
pub(super) fn submit_tcp_read(
session: u64,
stream: u64,
max_len: u32,
) -> NativeDataPlaneResult<u64> {
let stream = resource_id(stream)?;
get_session(session)?.submit(|core| core.submit_tcp_read(stream, max_len as usize))
}
pub(super) fn submit_tcp_write(
session: u64,
stream: u64,
data: Vec<u8>,
) -> NativeDataPlaneResult<u64> {
let stream = resource_id(stream)?;
get_session(session)?.submit(|core| core.submit_tcp_write(stream, data))
}
pub(super) fn submit_udp_bind(
session: u64,
local_port: u16,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
get_session(session)?.submit(|core| core.submit_udp_bind(local_port, timeout(timeout_ms)))
}
pub(super) fn submit_udp_receive(
session: u64,
socket: u64,
max_len: u32,
) -> NativeDataPlaneResult<u64> {
let socket = resource_id(socket)?;
get_session(session)?.submit(|core| core.submit_udp_receive(socket, max_len as usize))
}
pub(super) fn submit_udp_send(
session: u64,
socket: u64,
peer_addr: SocketAddr,
data: Vec<u8>,
) -> NativeDataPlaneResult<u64> {
let socket = resource_id(socket)?;
get_session(session)?.submit(|core| core.submit_udp_send(socket, peer_addr, data))
}
pub(super) fn set_resource_deadline(
session: u64,
resource: u64,
read: bool,
write: bool,
timeout_ms: u64,
) -> NativeDataPlaneResult<()> {
let resource = resource_id(resource)?;
get_session(session)?
.call(|core| core.set_resource_deadline(resource, read, write, timeout(timeout_ms)))
}
pub(super) fn cancel_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
let operation = operation_id(operation)?;
get_session(session)?.core.cancel_operation(operation);
Ok(())
}
pub(super) fn free_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
let operation = operation_id(operation)?;
get_session(session)?.core.free_operation(operation);
Ok(())
}
pub(super) fn close_resource(session: u64, resource: u64) -> NativeDataPlaneResult<()> {
let resource = resource_id(resource)?;
get_session(session)?.core.close_resource(resource);
Ok(())
}
pub(super) fn completion_wait(session: u64, timeout_ms: u64) -> NativeDataPlaneResult<bool> {
let session = get_session(session)?;
let ready = session.core.completion_wait(timeout(timeout_ms));
Ok(ready && !session.closed.load(Ordering::Acquire))
}
pub(super) fn drain_completions(
session: u64,
max_count: usize,
) -> NativeDataPlaneResult<Vec<DataPlaneCompletionDescriptor>> {
Ok(get_session(session)?.core.drain_completions(max_count))
}
pub(super) fn result_size(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
let operation = operation_id(operation)?;
get_session(session)?
.core
.result_payload_bytes(operation)
.map_err(Into::into)
}
fn take_result<T>(
session: u64,
operation: u64,
expected: DataPlaneOperationKind,
take: impl FnOnce(&DataPlaneOperationResult) -> Option<T>,
) -> NativeDataPlaneResult<T> {
let operation = operation_id(operation)?;
let session = get_session(session)?;
let actual = session.core.operation_kind(operation)?;
if actual != expected {
return Err(NativeDataPlaneError::invalid(format!(
"operation kind mismatch: expected {expected:?}, got {actual:?}"
)));
}
let result = session.core.take_result_with(operation, |outcome| {
Some(match outcome {
Ok(result) => take(result).ok_or_else(|| {
NativeDataPlaneError::invalid("data-plane result variant does not match operation")
}),
Err(kind) => Err(NativeDataPlaneError::new(
*kind,
format!("data-plane operation failed with {kind:?}"),
)),
})
})?;
result
.ok_or_else(|| NativeDataPlaneError::invalid("data-plane result could not be consumed"))?
}
pub(super) fn take_tcp_connect(
session: u64,
operation: u64,
) -> NativeDataPlaneResult<TcpConnectResult> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpConnect,
|result| match result {
DataPlaneOperationResult::TcpConnected {
stream,
local_addr,
peer_addr,
} => Some(TcpConnectResult {
stream: stream.get(),
local_addr: *local_addr,
peer_addr: *peer_addr,
}),
_ => None,
},
)
}
pub(super) fn take_tcp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<TcpBindResult> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpBind,
|result| match result {
DataPlaneOperationResult::TcpBound {
listener,
local_addr,
} => Some(TcpBindResult {
listener: listener.get(),
local_addr: *local_addr,
}),
_ => None,
},
)
}
pub(super) fn take_tcp_accept(
session: u64,
operation: u64,
) -> NativeDataPlaneResult<TcpAcceptResult> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpAccept,
|result| match result {
DataPlaneOperationResult::TcpAccepted {
stream,
local_addr,
peer_addr,
} => Some(TcpAcceptResult {
stream: stream.get(),
local_addr: *local_addr,
peer_addr: *peer_addr,
}),
_ => None,
},
)
}
pub(super) fn take_tcp_read(
session: u64,
operation: u64,
output: &mut [u8],
) -> NativeDataPlaneResult<TcpReadResult> {
let required = result_size(session, operation)?;
if output.len() < required {
return Err(NativeDataPlaneError::new(
DataPlaneErrorKind::BufferTooSmall,
format!(
"TCP read result requires {required} bytes, buffer has {}",
output.len()
),
));
}
take_result(
session,
operation,
DataPlaneOperationKind::TcpRead,
|result| match result {
DataPlaneOperationResult::TcpRead { data, eof } => {
output[..data.len()].copy_from_slice(data);
Some(TcpReadResult {
len: data.len(),
eof: *eof,
})
}
_ => None,
},
)
}
pub(super) fn take_tcp_write(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpWrite,
|result| match result {
DataPlaneOperationResult::TcpWritten { len } => Some(*len),
_ => None,
},
)
}
pub(super) fn take_udp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<UdpBindResult> {
take_result(
session,
operation,
DataPlaneOperationKind::UdpBind,
|result| match result {
DataPlaneOperationResult::UdpBound { socket, local_addr } => Some(UdpBindResult {
socket: socket.get(),
local_addr: *local_addr,
}),
_ => None,
},
)
}
pub(super) fn take_udp_receive(
session: u64,
operation: u64,
output: &mut [u8],
) -> NativeDataPlaneResult<UdpReceiveResult> {
let required = result_size(session, operation)?;
if output.len() < required {
return Err(NativeDataPlaneError::new(
DataPlaneErrorKind::BufferTooSmall,
format!(
"UDP receive result requires {required} bytes, buffer has {}",
output.len()
),
));
}
take_result(
session,
operation,
DataPlaneOperationKind::UdpReceive,
|result| match result {
DataPlaneOperationResult::UdpReceived {
data,
peer_addr,
truncated,
} => {
output[..data.len()].copy_from_slice(data);
Some(UdpReceiveResult {
len: data.len(),
peer_addr: *peer_addr,
truncated: *truncated,
})
}
_ => None,
},
)
}
pub(super) fn take_udp_send(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
take_result(
session,
operation,
DataPlaneOperationKind::UdpSend,
|result| match result {
DataPlaneOperationResult::UdpSent { len } => Some(*len),
_ => None,
},
)
}
pub(crate) fn remove_data_plane_sessions_by_instance_ids(ids: &[Uuid]) {
if ids.is_empty() {
return;
}
let _usage = DATA_PLANE_USAGE_LOCK
.write()
.unwrap_or_else(|error| error.into_inner());
let removed = {
let mut sessions = SESSIONS.lock().unwrap_or_else(|error| error.into_inner());
let handles = sessions
.iter()
.filter_map(|(handle, session)| ids.contains(&session.instance_id).then_some(*handle))
.collect::<Vec<_>>();
handles
.into_iter()
.filter_map(|handle| sessions.remove(&handle))
.collect::<Vec<_>>()
};
for session in removed {
session.close();
}
}
pub(crate) fn lock_for_config_server_start()
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
let guard = DATA_PLANE_USAGE_LOCK
.write()
.map_err(|error| format!("failed to lock data plane usage: {error}"))?;
if !SESSIONS
.lock()
.map_err(|error| format!("failed to lock data-plane sessions: {error}"))?
.is_empty()
{
return Err("cannot start config server client while data plane is in use".to_string());
}
Ok(guard)
}
#[cfg(test)]
mod tests {
use std::{sync::mpsc, time::Duration};
use super::*;
#[test]
fn config_server_start_waits_for_session_open_or_close() {
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
let (done_tx, done_rx) = mpsc::channel();
let waiter = std::thread::spawn(move || {
let _write_guard = lock_for_config_server_start().unwrap();
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();
waiter.join().unwrap();
}
}
File diff suppressed because it is too large Load Diff
+73 -133
View File
@@ -1,35 +1,35 @@
use std::ffi::{CString, c_char, c_int};
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
use easytier::common::config::ConfigLoader as _;
use easytier::common::config::{ConfigFileControl, TomlConfigLoader};
use crate::{
config_server::{
in_config_server_callback, remove_config_server_tracked_instance_ids,
wait_for_config_server_delivery,
},
data_plane::remove_data_plane_handles_by_instance_ids,
config_server::{in_config_server_callback, wait_for_config_server_delivery},
error::set_error_msg,
state::{
INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP, instance_name_exists,
lock_remote_instance_mutation,
},
state::{ffi_context, resolve_instance_id_by_name},
types::KeyValuePair,
};
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne")
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
fn mobile_tun_sources_for_legacy_set_tun_fd(
inst_id: &uuid::Uuid,
) -> Result<easytier::instance::virtual_nic::MobileTunSources, String> {
let Some(config) = INSTANCE_MANAGER.get_instance_config(inst_id) else {
return Ok(easytier::instance::virtual_nic::MobileTunSources::default());
};
fn mobile_tun_sources_for_legacy_set_tun_fd(inst_id: uuid::Uuid) -> Result<(), String> {
let config = ffi_context()
.manager
.config(inst_id)
.ok_or_else(|| format!("instance config unavailable: {inst_id}"))?;
let flags = config.get_flags();
if flags.dev_name.is_empty() {
return Ok(easytier::instance::virtual_nic::MobileTunSources::default());
return Ok(());
}
Err(format!(
@@ -47,36 +47,30 @@ pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
.to_string_lossy()
.into_owned()
};
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
set_error_msg(&format!("instance not found: {}", inst_name));
return -1;
}
let inst_id = *INSTANCE_NAME_ID_MAP
.get(&inst_name)
.as_ref()
.unwrap()
.value();
let inst_id = match resolve_instance_id_by_name(&inst_name) {
Ok(Some(instance_id)) => instance_id,
Ok(None) => {
set_error_msg(&format!("instance not found: {inst_name}"));
return -1;
}
Err(error) => {
set_error_msg(&error.to_string());
return -1;
}
};
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne")
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
let set_tun_fd_result = match mobile_tun_sources_for_legacy_set_tun_fd(&inst_id) {
Ok(sources) => INSTANCE_MANAGER
.set_tun_fd(&inst_id, fd, sources)
.map_err(|err| err.to_string()),
Err(err) => Err(err),
};
#[cfg(not(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne")
)))]
let set_tun_fd_result = INSTANCE_MANAGER.set_tun_fd(&inst_id, fd);
if let Err(error) = mobile_tun_sources_for_legacy_set_tun_fd(inst_id) {
set_error_msg(&error);
return -1;
}
match set_tun_fd_result {
match ffi_context().manager.attach_tun_fd(inst_id, fd) {
Ok(_) => 0,
Err(e) => {
set_error_msg(&format!("failed to set tun fd: {}", e));
@@ -125,34 +119,16 @@ pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> s
}
};
let inst_name = cfg.get_inst_name();
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 instance_name_exists(&inst_name) {
set_error_msg("instance already exists");
if let Err(e) = ffi_context().runtime.block_on(
ffi_context()
.process_management
.run_owned_network_instance(cfg, ConfigFileControl::STATIC_CONFIG),
) {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
let instance_id =
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
Ok(id) => id,
Err(e) => {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
};
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
0
}
@@ -196,50 +172,24 @@ pub(crate) unsafe fn retain_network_instance(
}
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));
let retained_names = if length == 0 {
Vec::new()
} else {
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
return -1;
}
};
inst_names
};
if length == 0 {
let removed_ids = INSTANCE_MANAGER.list_network_instance_ids();
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);
INSTANCE_NAME_ID_MAP.clear();
return 0;
}
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
return -1;
};
let removed_ids = INSTANCE_MANAGER
.list_network_instance_ids()
.into_iter()
.filter(|id| {
INSTANCE_MANAGER
.get_instance_name(id)
.is_none_or(|name| !inst_names.contains(&name))
})
.collect::<Vec<_>>();
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
set_error_msg(&format!("failed to delete instances: {}", e));
if let Err(error) = ffi_context().runtime.block_on(
ffi_context()
.process_management
.retain_owned_network_instances_by_name(retained_names),
) {
set_error_msg(&format!("failed to retain instances: {error}"));
return -1;
}
remove_config_server_tracked_instance_ids(&removed_ids);
remove_data_plane_handles_by_instance_ids(&removed_ids);
INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
0
}
@@ -255,15 +205,6 @@ pub(crate) unsafe fn delete_network_instance(
}
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;
}
@@ -272,22 +213,15 @@ pub(crate) unsafe fn delete_network_instance(
return -1;
};
let removed_ids = inst_names
.iter()
.filter_map(|name| INSTANCE_NAME_ID_MAP.get(name).map(|id| *id.value()))
.collect::<Vec<_>>();
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
set_error_msg(&format!("failed to delete instances: {}", e));
if let Err(error) = ffi_context().runtime.block_on(
ffi_context()
.process_management
.delete_owned_network_instances_by_name(inst_names),
) {
set_error_msg(&format!("failed to delete instances: {error}"));
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
}
@@ -311,7 +245,7 @@ pub(crate) unsafe fn collect_network_infos(
std::slice::from_raw_parts_mut(infos, max_length)
};
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
let collected_infos = match ffi_context().manager.collect_network_infos_sync() {
Ok(infos) => infos,
Err(e) => {
set_error_msg(&format!("failed to collect network infos: {}", e));
@@ -324,7 +258,11 @@ pub(crate) unsafe fn collect_network_infos(
if index >= max_length {
break;
}
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
let Some(key) = ffi_context()
.manager
.instance(*instance_id)
.map(|instance| instance.instance_name().to_owned())
else {
continue;
};
// convert value to json string
@@ -364,13 +302,15 @@ pub(crate) unsafe fn list_instance(infos: *mut KeyValuePair, max_length: usize)
}
let infos = unsafe { std::slice::from_raw_parts_mut(infos, max_length) };
let mut instances = INSTANCE_MANAGER
.list_network_instance_ids()
let mut instances = ffi_context()
.manager
.instance_ids()
.into_iter()
.filter_map(|id| {
INSTANCE_MANAGER
.get_instance_name(&id)
.map(|name| (name, id))
ffi_context()
.manager
.instance(id)
.map(|instance| (instance.instance_name().to_owned(), id))
})
.collect::<Vec<_>>();
instances.sort_by(|(left_name, left_id), (right_name, right_id)| {
+22 -15
View File
@@ -1,9 +1,12 @@
use std::ffi::{CString, c_char, c_int};
use std::{
ffi::{CString, c_char, c_int},
sync::Arc,
};
use crate::{
config_server::in_config_server_callback,
error::set_error_msg,
state::{ASYNC_RUNTIME, INSTANCE_MANAGER},
state::ffi_context,
strings::{c_str_to_string, optional_c_str_to_string},
};
@@ -65,19 +68,23 @@ pub(crate) unsafe fn call_json_rpc(
}
};
let response = match ASYNC_RUNTIME.block_on(easytier::rpc_service::call_json_rpc(
&INSTANCE_MANAGER,
&service_name,
&method_name,
domain_name.as_deref(),
payload,
)) {
Ok(value) => value,
Err(err) => {
set_error_msg(&format!("RPC Error: {}", err));
return -1;
}
};
let response =
match ffi_context()
.runtime
.block_on(easytier_core::management::call_management_json_rpc(
&ffi_context().manager,
Arc::new(easytier::rpc_service::logger::NativeLoggerControl),
&service_name,
&method_name,
domain_name.as_deref(),
payload,
)) {
Ok(value) => value,
Err(err) => {
set_error_msg(&format!("RPC Error: {}", err));
return -1;
}
};
let response_json = match serde_json::to_string(&response) {
Ok(value) => value,
Err(err) => {
+25 -641
View File
@@ -20,19 +20,12 @@
//! - `is_config_server_client_connected`: report whether the client is connected.
//!
//! Data plane APIs, enabled by the `ffi-dataplane` feature:
//! - `data_plane_tcp_connect`: open an outbound TCP data-plane stream.
//! - `data_plane_tcp_bind`: bind a TCP data-plane listener.
//! - `data_plane_tcp_accept`: accept a TCP data-plane connection.
//! - `data_plane_tcp_read`: read from a TCP data-plane stream.
//! - `data_plane_tcp_write`: write to a TCP data-plane stream.
//! - `data_plane_tcp_close`: close a TCP data-plane stream.
//! - `data_plane_tcp_listener_close`: close a TCP data-plane listener.
//! - `data_plane_udp_bind`: bind a UDP data-plane socket.
//! - `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.
//! - `data_plane_session_open` / `data_plane_session_close`: own one instance session.
//! - `data_plane_*_submit`: submit non-blocking TCP and UDP operations.
//! - `data_plane_completion_wait` / `data_plane_completion_drain`: await completions.
//! - `data_plane_*_result_take`: consume typed operation results.
//! - `data_plane_operation_cancel` / `data_plane_operation_free`: control operations.
//! - `data_plane_resource_close`: close streams, listeners, and UDP sockets.
//!
//! Shared FFI helper APIs:
//! - `get_error_msg`: copy the last FFI or config-server callback error message.
@@ -40,8 +33,6 @@
mod config_server;
mod data_plane;
#[cfg(feature = "ffi-dataplane")]
mod data_plane_async;
mod error;
mod instance_api;
mod json_rpc;
@@ -53,11 +44,11 @@ mod types;
mod tests;
pub use config_server::{in_config_server_callback, validate_config_server_client_options};
pub use types::{ConfigServerEventCallback, KeyValuePair};
pub use types::{
ConfigServerEventCallback, DataPlaneCompletion, DataPlaneSocketAddr, KeyValuePair,
};
use std::ffi::{c_char, c_int, c_void};
#[cfg(feature = "ffi-dataplane")]
use std::ffi::{c_uchar, c_ushort};
// ===== Network Management API =====
@@ -254,7 +245,7 @@ pub unsafe extern "C" fn call_json_rpc(
/// Start the managed config-server client.
///
/// The client reuses EasyTier's web-client path and applies remote config
/// changes through the shared `NetworkInstanceManager`. Successful remote run
/// changes through the shared `NativeInstanceManager`. Successful remote run
/// and delete operations are delivered to `callback` as JSON event strings, one
/// callback per affected instance. The event string is valid only for the
/// duration of the callback; callers must copy it if they need to keep it.
@@ -319,634 +310,27 @@ pub extern "C" fn is_config_server_client_connected() -> c_int {
// ===== Data Plane API =====
/// Open an outbound TCP stream through an EasyTier instance data plane.
///
/// On success, writes the local address selected for the connection into
/// `out_local_ip` and `out_local_port`. The returned IP string is allocated by
/// this library and must be released with `free_string`.
///
/// The data plane is mutually exclusive with the config-server client. This
/// function returns `0` if the config-server client is active or stopping.
///
/// # Safety
/// `inst_name`, `dst_ip`, `out_local_ip`, and `out_local_port` must be non-null.
/// String pointers must point to null-terminated UTF-8 strings.
///
/// # Return
/// Returns a non-zero TCP stream handle on success, or `0` on failure. On
/// failure, call `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_connect(
inst_name: *const c_char,
dst_ip: *const c_char,
dst_port: c_ushort,
timeout_ms: u64,
out_local_ip: *mut *const c_char,
out_local_port: *mut c_ushort,
) -> u64 {
unsafe {
data_plane::data_plane_tcp_connect(
inst_name,
dst_ip,
dst_port,
timeout_ms,
out_local_ip,
out_local_port,
)
}
}
/// Bind a TCP listener through an EasyTier instance data plane.
///
/// On success, writes the bound local address into `out_local_ip` and
/// `out_local_port`. The returned IP string is allocated by this library and
/// must be released with `free_string`.
///
/// # Safety
/// `inst_name`, `out_local_ip`, and `out_local_port` must be non-null.
/// `inst_name` must point to a null-terminated UTF-8 string.
///
/// # Return
/// Returns a non-zero TCP listener handle on success, or `0` on failure. On
/// failure, call `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_bind(
inst_name: *const c_char,
local_port: c_ushort,
timeout_ms: u64,
out_local_ip: *mut *const c_char,
out_local_port: *mut c_ushort,
) -> u64 {
unsafe {
data_plane::data_plane_tcp_bind(
inst_name,
local_port,
timeout_ms,
out_local_ip,
out_local_port,
)
}
}
/// Accept one connection from a TCP data-plane listener.
///
/// On success, writes both local and peer socket addresses to the output
/// pointers. Returned IP strings are allocated by this library and must be
/// released with `free_string`.
///
/// # Safety
/// All output pointers must be non-null and writable. `handle` must be a valid
/// TCP listener handle returned by `data_plane_tcp_bind`.
///
/// # Return
/// Returns a non-zero TCP stream handle on success, or `0` on failure. On
/// failure, call `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_accept(
handle: u64,
timeout_ms: 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::data_plane_tcp_accept(
handle,
timeout_ms,
out_local_ip,
out_local_port,
out_peer_ip,
out_peer_port,
)
}
}
/// Read bytes from a TCP data-plane stream.
///
/// # Safety
/// `handle` must be a valid TCP stream handle returned by
/// `data_plane_tcp_connect` or `data_plane_tcp_accept`. `buf` must be non-null
/// and writable for `len` bytes.
///
/// # Return
/// Returns the number of bytes read, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_read(
handle: u64,
buf: *mut c_uchar,
len: u32,
timeout_ms: u64,
) -> c_int {
unsafe { data_plane::data_plane_tcp_read(handle, buf, len, timeout_ms) }
}
/// Write bytes to a TCP data-plane stream.
///
/// This function attempts to write exactly `len` bytes before returning
/// success.
///
/// # Safety
/// `handle` must be a valid TCP stream handle returned by
/// `data_plane_tcp_connect` or `data_plane_tcp_accept`. `buf` must be non-null
/// and readable for `len` bytes.
///
/// # Return
/// Returns `len` on success, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_write(
handle: u64,
buf: *const c_uchar,
len: u32,
timeout_ms: u64,
) -> c_int {
unsafe { data_plane::data_plane_tcp_write(handle, buf, len, timeout_ms) }
}
/// Close a TCP data-plane stream handle.
///
/// # Return
/// Returns `0` on success, or `-1` if the handle is missing, is not a TCP stream
/// handle, or data-plane calls are currently rejected.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_tcp_close(handle: u64) -> c_int {
data_plane::data_plane_tcp_close(handle)
}
/// Close a TCP data-plane listener handle.
///
/// # Return
/// Returns `0` on success, or `-1` if the handle is missing, is not a TCP
/// listener handle, or data-plane calls are currently rejected.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_tcp_listener_close(handle: u64) -> c_int {
data_plane::data_plane_tcp_listener_close(handle)
}
/// Bind a UDP socket through an EasyTier instance data plane.
///
/// On success, writes the bound local address into `out_local_ip` and
/// `out_local_port`. The returned IP string is allocated by this library and
/// must be released with `free_string`.
///
/// # Safety
/// `inst_name`, `out_local_ip`, and `out_local_port` must be non-null.
/// `inst_name` must point to a null-terminated UTF-8 string.
///
/// # Return
/// Returns a non-zero UDP socket handle on success, or `0` on failure. On
/// failure, call `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_bind(
inst_name: *const c_char,
local_port: c_ushort,
timeout_ms: u64,
out_local_ip: *mut *const c_char,
out_local_port: *mut c_ushort,
) -> u64 {
unsafe {
data_plane::data_plane_udp_bind(
inst_name,
local_port,
timeout_ms,
out_local_ip,
out_local_port,
)
}
}
/// Send one UDP datagram through a data-plane socket.
///
/// # Safety
/// `handle` must be a valid UDP socket handle returned by
/// `data_plane_udp_bind`. `dst_ip` must be non-null and point to a
/// null-terminated UTF-8 string. `buf` must be non-null and readable for `len`
/// bytes.
///
/// # Return
/// Returns the number of bytes sent, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_send_to(
handle: u64,
dst_ip: *const c_char,
dst_port: c_ushort,
buf: *const c_uchar,
len: u32,
timeout_ms: u64,
) -> c_int {
unsafe { data_plane::data_plane_udp_send_to(handle, dst_ip, dst_port, buf, len, timeout_ms) }
}
/// Receive one UDP datagram from a data-plane socket.
///
/// On success, writes the peer address into `out_ip` and `out_port`. The
/// returned IP string is allocated by this library and must be released with
/// `free_string`.
///
/// # Safety
/// `handle` must be a valid UDP socket handle returned by
/// `data_plane_udp_bind`. `buf`, `out_ip`, and `out_port` must be non-null.
/// `buf` must be writable for `len` bytes.
///
/// # Return
/// Returns the number of bytes received, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_recv_from(
handle: u64,
buf: *mut c_uchar,
len: u32,
out_ip: *mut *const c_char,
out_port: *mut c_ushort,
timeout_ms: u64,
) -> c_int {
unsafe { data_plane::data_plane_udp_recv_from(handle, buf, len, out_ip, out_port, timeout_ms) }
}
/// Close a UDP data-plane socket handle.
///
/// # Return
/// Returns `0` on success, or `-1` if the handle is missing, is not a UDP
/// socket handle, or data-plane calls are currently rejected.
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
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)
}
/// Start an asynchronous TCP data-plane connection.
///
/// # Safety
/// `inst_name` and `dst_ip` must be non-null pointers to null-terminated UTF-8
/// strings. The strings only need to remain valid for the duration of this
/// call.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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)
}
}
/// Finish an asynchronous TCP data-plane connection.
///
/// On success, writes the stream local address into `out_local_ip` and
/// `out_local_port`. The returned IP string is allocated by this library and
/// must be released with `free_string`.
///
/// # Safety
/// `out_local_ip` and `out_local_port` must be non-null pointers to writable
/// storage.
///
/// # Return
/// Returns a non-zero TCP stream handle on success, or `0` on failure.
#[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)
}
}
/// Start an asynchronous TCP data-plane bind.
///
/// # Safety
/// `inst_name` must be a non-null pointer to a null-terminated UTF-8 string.
/// The string only needs to remain valid for the duration of this call.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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) }
}
/// Finish an asynchronous TCP data-plane bind.
///
/// On success, writes the listener local address into `out_local_ip` and
/// `out_local_port`. The returned IP string is allocated by this library and
/// must be released with `free_string`.
///
/// # Safety
/// `out_local_ip` and `out_local_port` must be non-null pointers to writable
/// storage.
///
/// # Return
/// Returns a non-zero TCP listener handle on success, or `0` on failure.
#[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) }
}
/// Start an asynchronous TCP data-plane accept on a listener handle.
///
/// # Safety
/// `handle` must be a valid TCP listener handle returned by
/// `data_plane_tcp_bind` or `data_plane_tcp_bind_finish`.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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) }
}
/// Finish an asynchronous TCP data-plane accept.
///
/// On success, writes the accepted stream local address into `out_local_ip` and
/// `out_local_port`, and the peer address into `out_peer_ip` and
/// `out_peer_port`. Returned IP strings are allocated by this library and must
/// be released with `free_string`.
///
/// # Safety
/// `out_local_ip`, `out_local_port`, `out_peer_ip`, and `out_peer_port` must be
/// non-null pointers to writable storage.
///
/// # Return
/// Returns a non-zero TCP stream handle on success, or `0` on failure.
#[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,
)
}
}
/// Start an asynchronous TCP data-plane read.
///
/// # Safety
/// `handle` must be a valid TCP stream handle returned by
/// `data_plane_tcp_connect_finish` or `data_plane_tcp_accept_finish`.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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) }
}
/// Finish an asynchronous TCP data-plane read.
///
/// On success, writes the received buffer pointer and length into `out_buf` and
/// `out_len`. The returned buffer is allocated by this library and must be
/// released with `data_plane_free_bytes`.
///
/// # Safety
/// `out_buf` and `out_len` must be non-null pointers to writable storage.
///
/// # Return
/// Returns the number of bytes read, or `-1` on failure.
#[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) }
}
/// Start an asynchronous TCP data-plane write.
///
/// The input bytes are copied before this function returns.
///
/// # Safety
/// `handle` must be a valid TCP stream handle returned by
/// `data_plane_tcp_connect_finish` or `data_plane_tcp_accept_finish`. If `len`
/// is non-zero, `buf` must be non-null and readable for `len` bytes.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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)
}
/// Start an asynchronous UDP data-plane bind.
///
/// # Safety
/// `inst_name` must be a non-null pointer to a null-terminated UTF-8 string.
/// The string only needs to remain valid for the duration of this call.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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) }
}
/// Finish an asynchronous UDP data-plane bind.
///
/// On success, writes the socket local address into `out_local_ip` and
/// `out_local_port`. The returned IP string is allocated by this library and
/// must be released with `free_string`.
///
/// # Safety
/// `out_local_ip` and `out_local_port` must be non-null pointers to writable
/// storage.
///
/// # Return
/// Returns a non-zero UDP socket handle on success, or `0` on failure.
#[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) }
}
/// Start an asynchronous UDP data-plane send.
///
/// The input bytes are copied before this function returns.
///
/// # Safety
/// `handle` must be a valid UDP socket handle returned by
/// `data_plane_udp_bind_finish`. `dst_ip` must be a non-null pointer to a
/// null-terminated UTF-8 string. If `len` is non-zero, `buf` must be non-null
/// and readable for `len` bytes.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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)
}
/// Start an asynchronous UDP data-plane receive.
///
/// # Safety
/// `handle` must be a valid UDP socket handle returned by
/// `data_plane_udp_bind_finish`.
///
/// # Return
/// Returns a non-zero async operation handle on success, or `0` on failure.
#[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) }
}
/// Finish an asynchronous UDP data-plane receive.
///
/// On success, writes the received buffer into `out_buf` and `out_len`, and
/// the peer address into `out_ip` and `out_port`. The returned buffer is
/// allocated by this library and must be released with `data_plane_free_bytes`;
/// the returned IP string must be released with `free_string`.
///
/// # Safety
/// `out_buf`, `out_len`, `out_ip`, and `out_port` must be non-null pointers to
/// writable storage.
///
/// # Return
/// Returns the number of bytes received, or `-1` on failure.
#[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,
)
}
}
pub use data_plane::{
DATA_PLANE_DEADLINE_READ, DATA_PLANE_DEADLINE_WRITE, data_plane_completion_drain,
data_plane_completion_wait, data_plane_operation_cancel, data_plane_operation_free,
data_plane_resource_close, data_plane_resource_deadline_set, data_plane_result_size,
data_plane_session_close, data_plane_session_open, data_plane_tcp_accept_result_take,
data_plane_tcp_accept_submit, data_plane_tcp_bind_result_take, data_plane_tcp_bind_submit,
data_plane_tcp_connect_result_take, data_plane_tcp_connect_submit,
data_plane_tcp_read_result_take, data_plane_tcp_read_submit, data_plane_tcp_write_result_take,
data_plane_tcp_write_submit, data_plane_udp_bind_result_take, data_plane_udp_bind_submit,
data_plane_udp_receive_result_take, data_plane_udp_receive_submit,
data_plane_udp_send_result_take, data_plane_udp_send_submit,
};
// ===== Shared FFI Helper API =====
/// Return the last FFI error message.
///
/// Synchronous API failures are stored in a thread-local buffer, so call this
/// on the same thread that received `-1` or `0` from another API. Config-server
/// API failures are stored in a thread-local buffer, so call this on the same
/// thread that received a negative status or another documented failure
/// sentinel. Config-server
/// callback delivery failures may happen on a runtime thread; those are stored
/// globally and are included here so direct FFI callers can still retrieve the
/// last callback error. If there is no error message, this writes a null pointer
+53 -41
View File
@@ -1,54 +1,66 @@
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use dashmap::DashMap;
use easytier::instance_manager::NetworkInstanceManager;
use easytier::instance::factory::{
NativeInstanceManager, NativeProcessManagement, native_instance_manager_with_runtime,
native_process_management,
};
use tokio::runtime::{Builder, Runtime};
use uuid::Uuid;
pub(crate) static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, Uuid>> =
once_cell::sync::Lazy::new(DashMap::new);
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
pub(crate) static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> =
once_cell::sync::Lazy::new(|| {
Builder::new_multi_thread()
struct FfiOwnedInstanceHooks;
#[async_trait::async_trait]
impl easytier_core::management::InstanceMutationHooks for FfiOwnedInstanceHooks {
async fn post_remove_network_instances(
&self,
instance_ids: &[uuid::Uuid],
) -> Result<(), String> {
crate::config_server::remove_config_server_tracked_instance_ids(instance_ids);
crate::data_plane::remove_data_plane_sessions_by_instance_ids(instance_ids);
Ok(())
}
}
pub(crate) struct FfiContext {
pub(crate) runtime: Runtime,
pub(crate) manager: Arc<NativeInstanceManager>,
pub(crate) process_management: NativeProcessManagement,
}
impl FfiContext {
fn new() -> Self {
let runtime = Builder::new_multi_thread()
.enable_all()
.build()
.expect("tokio runtime for easytier-ffi")
});
pub(crate) static INSTANCE_MUTATION_LOCK: once_cell::sync::Lazy<Mutex<()>> =
once_cell::sync::Lazy::new(|| Mutex::new(()));
pub(crate) fn remove_instance_name_ids(ids: &[Uuid]) {
if ids.is_empty() {
return;
.expect("tokio runtime for easytier-ffi");
let manager = Arc::new(native_instance_manager_with_runtime(
runtime.handle().clone(),
));
let process_management =
native_process_management(manager.clone(), Arc::new(FfiOwnedInstanceHooks));
Self {
runtime,
manager,
process_management,
}
}
INSTANCE_NAME_ID_MAP.retain(|_, instance_id| !ids.contains(instance_id));
}
pub(crate) fn lock_remote_instance_mutation() -> tokio::sync::OwnedMutexGuard<()> {
INSTANCE_MANAGER
.remote_mutation_lock()
.blocking_lock_owned()
static FFI_CONTEXT: once_cell::sync::Lazy<FfiContext> = once_cell::sync::Lazy::new(FfiContext::new);
pub(crate) fn ffi_context() -> &'static FfiContext {
&FFI_CONTEXT
}
pub(crate) fn instance_name_exists(inst_name: &str) -> bool {
find_instance_id_by_name(inst_name).is_some()
pub(crate) fn resolve_instance_id_by_name(inst_name: &str) -> Result<Option<uuid::Uuid>, String> {
easytier_core::management::resolve_optional_instance_by_name(
ffi_context().manager.as_ref(),
inst_name,
)
.map(|instance| instance.map(|instance| instance.instance_id()))
.map_err(|error| error.to_string())
}
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<Uuid> {
INSTANCE_NAME_ID_MAP
.get(inst_name)
.map(|id| *id)
.or_else(|| {
INSTANCE_MANAGER
.list_network_instance_ids()
.into_iter()
.find(|id| {
INSTANCE_MANAGER
.get_instance_name(id)
.is_some_and(|name| name == inst_name)
})
})
#[cfg(test)]
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<uuid::Uuid> {
resolve_instance_id_by_name(inst_name).ok().flatten()
}
+173 -193
View File
@@ -2,10 +2,7 @@ use crate::{
config_server::{
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
},
state::{
INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP, find_instance_id_by_name,
lock_remote_instance_mutation, remove_instance_name_ids,
},
state::{ffi_context, find_instance_id_by_name},
*,
};
use easytier::{
@@ -15,7 +12,7 @@ use easytier::{
use serde_json::Value;
use std::{
collections::HashSet,
ffi::{CStr, CString, c_char, c_void},
ffi::{CStr, CString, c_char, c_int, c_void},
sync::{Mutex, mpsc},
time::Duration,
};
@@ -101,10 +98,10 @@ fn list_instance_returns_instance_names_and_ids() {
let cfg = TomlConfigLoader::default();
cfg.set_id(instance_id);
cfg.set_inst_name(instance_name.clone());
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
INSTANCE_NAME_ID_MAP.insert(instance_name.clone(), instance_id);
let mut infos = vec![
KeyValuePair {
@@ -127,10 +124,14 @@ fn list_instance_returns_instance_names_and_ids() {
}
free_key_value_pairs(&infos[..count as usize]);
INSTANCE_MANAGER
.delete_network_instance(vec![instance_id])
ffi_context()
.runtime
.block_on(
ffi_context()
.manager
.delete_network_instances([instance_id]),
)
.unwrap();
remove_instance_name_ids(&[instance_id]);
assert!(found);
}
@@ -261,8 +262,9 @@ async fn config_server_hooks_emit_run_event() {
let inst_name = format!("test-{}", instance_id);
cfg.set_inst_name(inst_name.clone());
hooks.pre_run_network_instance(&cfg).await.unwrap();
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
hooks.post_run_network_instance(&instance_id).await.unwrap();
@@ -278,17 +280,18 @@ async fn config_server_hooks_emit_run_event() {
);
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
let events = events.lock().unwrap();
let events = events.lock().unwrap().clone();
assert_eq!(events.len(), 1);
let event: Value = serde_json::from_str(&events[0]).unwrap();
assert_eq!(event["event"], "run_network_instance");
assert_eq!(event["success"], true);
assert_eq!(event["instance_id"], instance_id.to_string());
assert!(event["error"].is_null());
INSTANCE_MANAGER
.delete_network_instance(vec![instance_id])
ffi_context()
.manager
.delete_network_instances([instance_id])
.await
.unwrap();
remove_instance_name_ids(&[instance_id]);
}
#[tokio::test]
@@ -306,8 +309,9 @@ async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
cfg.set_id(id);
cfg.set_inst_name(format!("test-{}", id));
hooks.pre_run_network_instance(&cfg).await.unwrap();
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
}
@@ -327,7 +331,7 @@ async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
.unwrap();
assert!(hooks.tracked_instance_ids().is_empty());
let events = events.lock().unwrap();
let events = events.lock().unwrap().clone();
assert_eq!(events.len(), 2);
let event_ids = events
.iter()
@@ -343,29 +347,27 @@ async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
event_ids,
HashSet::from([instance_id_1.to_string(), instance_id_2.to_string()])
);
INSTANCE_MANAGER
.delete_network_instance(vec![instance_id_1, instance_id_2])
ffi_context()
.manager
.delete_network_instances([instance_id_1, instance_id_2])
.await
.unwrap();
remove_instance_name_ids(&[instance_id_1, instance_id_2]);
}
#[tokio::test]
async fn config_server_hooks_remove_untracked_name_mapping_without_event() {
async fn config_server_hooks_ignore_untracked_instance_without_event() {
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
let hooks = ManagedConfigServerClientHooks::new(
Some(record_config_server_event),
&events as *const _ as *mut c_void,
);
let local_id = Uuid::new_v4();
let inst_name = format!("local-{}", local_id);
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), local_id);
hooks
.post_remove_network_instances(&[local_id])
.await
.unwrap();
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
assert!(events.lock().unwrap().is_empty());
}
@@ -375,15 +377,25 @@ async fn config_server_hooks_reject_duplicate_instance_name() {
let inst_name = format!("test-{}", Uuid::new_v4());
let existing_id = Uuid::new_v4();
let new_id = Uuid::new_v4();
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), existing_id);
let existing_cfg = TomlConfigLoader::default();
existing_cfg.set_inst_name(inst_name.clone());
existing_cfg.set_id(existing_id);
ffi_context()
.manager
.run_network_instance(existing_cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
let cfg = TomlConfigLoader::default();
cfg.set_inst_name(inst_name.clone());
cfg.set_id(new_id);
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
assert_eq!(*INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap(), existing_id);
INSTANCE_NAME_ID_MAP.remove(&inst_name);
assert_eq!(find_instance_id_by_name(&inst_name), Some(existing_id));
ffi_context()
.manager
.delete_network_instances([existing_id])
.await
.unwrap();
}
#[tokio::test]
@@ -398,8 +410,23 @@ async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error()
let overwritten_id = Uuid::new_v4();
let duplicate_id = Uuid::new_v4();
hooks.instance_ids.lock().unwrap().insert(overwritten_id);
INSTANCE_NAME_ID_MAP.insert(old_name.clone(), overwritten_id);
INSTANCE_NAME_ID_MAP.insert(duplicate_name.clone(), duplicate_id);
for (id, name) in [
(overwritten_id, old_name.clone()),
(duplicate_id, duplicate_name.clone()),
] {
let cfg = TomlConfigLoader::default();
cfg.set_id(id);
cfg.set_inst_name(name);
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
}
ffi_context()
.manager
.delete_network_instances([overwritten_id])
.await
.unwrap();
hooks
.post_remove_network_instances(&[overwritten_id])
@@ -412,13 +439,17 @@ async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error()
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
assert!(hooks.tracked_instance_ids().is_empty());
assert!(INSTANCE_NAME_ID_MAP.get(&old_name).is_none());
assert!(find_instance_id_by_name(&old_name).is_none());
assert_eq!(
*INSTANCE_NAME_ID_MAP.get(&duplicate_name).unwrap(),
duplicate_id
find_instance_id_by_name(&duplicate_name),
Some(duplicate_id)
);
assert_eq!(events.lock().unwrap().len(), 1);
INSTANCE_NAME_ID_MAP.remove(&duplicate_name);
ffi_context()
.manager
.delete_network_instances([duplicate_id])
.await
.unwrap();
}
#[tokio::test]
@@ -427,11 +458,19 @@ async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
let inst_name = format!("test-{}", Uuid::new_v4());
let instance_id = Uuid::new_v4();
hooks.instance_ids.lock().unwrap().insert(instance_id);
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), instance_id);
let cfg = TomlConfigLoader::default();
cfg.set_inst_name(inst_name.clone());
cfg.set_id(instance_id);
ffi_context()
.manager
.run_network_instance(cfg.clone(), ConfigFileControl::STATIC_CONFIG)
.unwrap();
ffi_context()
.manager
.delete_network_instances([instance_id])
.await
.unwrap();
hooks
.post_remove_network_instances(&[instance_id])
@@ -440,7 +479,7 @@ async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
hooks.pre_run_network_instance(&cfg).await.unwrap();
assert!(hooks.tracked_instance_ids().is_empty());
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
assert!(find_instance_id_by_name(&inst_name).is_none());
}
#[tokio::test]
@@ -451,11 +490,14 @@ async fn config_server_hooks_reject_post_run_after_external_delete() {
cfg.set_id(instance_id);
cfg.set_inst_name(format!("test-{}", instance_id));
hooks.pre_run_network_instance(&cfg).await.unwrap();
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
INSTANCE_MANAGER
.delete_network_instance(vec![instance_id])
ffi_context()
.manager
.delete_network_instances([instance_id])
.await
.unwrap();
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
@@ -468,15 +510,20 @@ fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
let cfg = TomlConfigLoader::default();
cfg.set_id(instance_id);
cfg.set_inst_name(inst_name.clone());
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
INSTANCE_MANAGER
.delete_network_instance(vec![instance_id])
ffi_context()
.runtime
.block_on(
ffi_context()
.manager
.delete_network_instances([instance_id]),
)
.unwrap();
remove_instance_name_ids(&[instance_id]);
}
#[test]
@@ -493,10 +540,10 @@ fn delete_network_instance_removes_only_named_instances() {
let cfg = TomlConfigLoader::default();
cfg.set_id(id);
cfg.set_inst_name(name.clone());
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
INSTANCE_NAME_ID_MAP.insert(name, id);
}
let delete_name = CString::new(delete_name.clone()).unwrap();
@@ -509,10 +556,10 @@ fn delete_network_instance_removes_only_named_instances() {
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])
ffi_context()
.runtime
.block_on(ffi_context().manager.delete_network_instances([keep_id]))
.unwrap();
remove_instance_name_ids(&[keep_id]);
}
#[test]
@@ -532,13 +579,18 @@ fn retain_and_delete_network_instance_reject_invalid_name_pointers() {
}
#[test]
fn ffi_remote_mutation_lock_uses_manager_lock() {
let manager_guard = INSTANCE_MANAGER
.remote_mutation_lock()
.blocking_lock_owned();
fn ffi_process_management_uses_manager_mutation_lock() {
let manager_guard = ffi_context().manager.mutation_lock().blocking_lock_owned();
let (done_tx, done_rx) = mpsc::channel();
let waiter = std::thread::spawn(move || {
let _ffi_guard = lock_remote_instance_mutation();
ffi_context()
.runtime
.block_on(
ffi_context()
.process_management
.delete_owned_network_instances(Vec::new()),
)
.unwrap();
done_tx.send(()).unwrap();
});
@@ -549,7 +601,7 @@ fn ffi_remote_mutation_lock_uses_manager_lock() {
}
#[tokio::test]
async fn config_server_hooks_suppress_late_run_events_while_stopping() {
async fn config_server_hooks_reject_late_runs_for_core_rollback() {
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
let hooks = ManagedConfigServerClientHooks::new(
Some(record_config_server_event),
@@ -557,15 +609,54 @@ async fn config_server_hooks_suppress_late_run_events_while_stopping() {
);
hooks.start_stopping();
hooks
.post_run_network_instance(&Uuid::new_v4())
.await
.unwrap();
assert!(
hooks
.post_run_network_instance(&Uuid::new_v4())
.await
.is_err()
);
assert!(hooks.tracked_instance_ids().is_empty());
assert!(events.lock().unwrap().is_empty());
}
#[test]
fn delete_network_instance_rejects_an_ambiguous_name() {
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
let instance_ids = [Uuid::new_v4(), Uuid::new_v4()];
for instance_id in instance_ids {
let config = TomlConfigLoader::default();
config.set_id(instance_id);
config.set_inst_name(duplicate_name.clone());
ffi_context()
.manager
.run_network_instance(config, ConfigFileControl::STATIC_CONFIG)
.unwrap();
}
let duplicate_name = CString::new(duplicate_name).unwrap();
let names = [duplicate_name.as_ptr()];
assert_eq!(
unsafe { delete_network_instance(names.as_ptr(), names.len()) },
-1
);
assert!(take_last_error().unwrap().contains("2 instances match"));
assert!(
instance_ids
.iter()
.all(|id| ffi_context().manager.instance(*id).is_some())
);
ffi_context()
.runtime
.block_on(
ffi_context()
.process_management
.delete_owned_network_instances(instance_ids.to_vec()),
)
.unwrap();
}
#[test]
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
let _callback_scope = ConfigServerCallbackScope::enter();
@@ -615,112 +706,12 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
#[cfg(feature = "ffi-dataplane")]
{
let mut session = 0;
assert_eq!(
unsafe {
data_plane_tcp_connect(
std::ptr::null(),
std::ptr::null(),
0,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0
unsafe { data_plane_session_open(std::ptr::null(), &mut session) },
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
);
assert_eq!(
unsafe {
data_plane_tcp_bind(
std::ptr::null(),
0,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0
);
assert_eq!(
unsafe {
data_plane_tcp_accept(
0,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0
);
assert_eq!(
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
-1
);
assert_eq!(
unsafe { data_plane_tcp_write(0, std::ptr::null(), 0, 0) },
-1
);
assert_eq!(data_plane_tcp_close(0), -1);
assert_eq!(data_plane_tcp_listener_close(0), -1);
assert_eq!(
unsafe {
data_plane_udp_bind(
std::ptr::null(),
0,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0
);
assert_eq!(
unsafe { data_plane_udp_send_to(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
-1
);
assert_eq!(
unsafe {
data_plane_udp_recv_from(
0,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
)
},
-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);
assert_eq!(session, 0);
}
}
@@ -729,38 +720,27 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
fn active_config_server_rejects_data_plane() {
set_active_for_test(true);
let name = CString::new("missing").unwrap();
let mut session = 0;
assert_eq!(
unsafe {
data_plane_tcp_connect(
std::ptr::null(),
std::ptr::null(),
0,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0
unsafe { data_plane_session_open(name.as_ptr(), &mut session) },
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
);
assert_eq!(
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);
assert_eq!(session, 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);
fn data_plane_invalid_handle_errors_are_stable() {
let closed = -(easytier_core::gateway::DataPlaneErrorKind::HandleClosed as c_int);
assert_eq!(data_plane_completion_wait(u64::MAX, 0), closed);
assert_eq!(data_plane_operation_cancel(u64::MAX, 1), closed);
assert_eq!(data_plane_operation_free(u64::MAX, 1), closed);
assert_eq!(data_plane_resource_close(u64::MAX, 1), closed);
assert_eq!(
data_plane_resource_deadline_set(u64::MAX, 1, DATA_PLANE_DEADLINE_READ, 0),
closed
);
}
@@ -8,3 +8,23 @@ pub struct KeyValuePair {
}
pub type ConfigServerEventCallback = Option<unsafe extern "C" fn(*const c_char, *mut c_void)>;
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DataPlaneSocketAddr {
/// `4` for IPv4. Other families are reserved for later ABI versions.
pub family: u16,
/// Native-endian port number.
pub port: u16,
/// Network-order address bytes. IPv4 uses the first four bytes.
pub address: [u8; 16],
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DataPlaneCompletion {
pub operation_id: u64,
pub operation_kind: u16,
/// `0` for success, otherwise a stable `DataPlaneErrorKind` value.
pub status: u16,
}