mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-05 10:35:38 +00:00
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:
@@ -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`
|
||||
@@ -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
@@ -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)| {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user