mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 17:45:44 +00:00
Add FFI JNI JSON RPC bridge (#2326)
* Add FFI JNI JSON RPC bridge * Add FFI instance list API
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
- 📱 原生 Android JNI 支持
|
- 📱 原生 Android JNI 支持
|
||||||
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
|
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
|
||||||
- 🛡️ 类型安全的 Java 接口
|
- 🛡️ 类型安全的 Java 接口
|
||||||
|
- 🔌 支持通过 JSON 调用已暴露的 EasyTier RPC 查询/管理接口
|
||||||
- 📝 详细的错误处理和日志记录
|
- 📝 详细的错误处理和日志记录
|
||||||
|
|
||||||
## 支持的架构
|
## 支持的架构
|
||||||
@@ -176,6 +177,20 @@ public class EasyTierManager {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 通用 JSON RPC
|
||||||
|
|
||||||
|
`EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson)` 可以调用已暴露的
|
||||||
|
EasyTier RPC 服务,payload 和返回值均为 protobuf JSON。该接口不支持
|
||||||
|
`api.manage.WebClientService`;实例启动、保留、删除、信息收集仍使用专用 JNI API。
|
||||||
|
|
||||||
|
```java
|
||||||
|
String response = EasyTierJNI.callJsonRpc(
|
||||||
|
"api.logger.LoggerRpcService",
|
||||||
|
"get_logger_config",
|
||||||
|
"{}"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
### VPN 服务集成
|
### VPN 服务集成
|
||||||
|
|
||||||
如果您要在 Android VPN 服务中使用:
|
如果您要在 Android VPN 服务中使用:
|
||||||
|
|||||||
@@ -81,6 +81,43 @@ object EasyTierJNI {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出当前运行的实例名称和实例 ID。
|
||||||
|
* @param maxLength 最大返回条目数
|
||||||
|
* @return JSON 对象,key 为 instance name,value 为 instance id
|
||||||
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun listInstances(maxLength: Int): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用暴露的 EasyTier RPC 方法,输入和输出均为 protobuf JSON 字符串。
|
||||||
|
*
|
||||||
|
* 不支持 api.manage.WebClientService;实例启动、保留、删除、信息收集请继续使用专用 JNI API。
|
||||||
|
* payloadJson 需要包含目标 RPC 所需的 instance selector。
|
||||||
|
*
|
||||||
|
* @param serviceName RPC 服务名,例如 api.instance.PeerManageRpcService
|
||||||
|
* @param methodName RPC 方法名,支持 snake_case 或 proto 方法名
|
||||||
|
* @param domainName 仅 TcpProxyRpcService 使用;传 null 或空字符串默认 tcp
|
||||||
|
* @param payloadJson protobuf JSON 请求体
|
||||||
|
* @return protobuf JSON 响应体
|
||||||
|
* @throws RuntimeException 当 RPC 调用失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
external fun callJsonRpc(
|
||||||
|
serviceName: String,
|
||||||
|
methodName: String,
|
||||||
|
domainName: String?,
|
||||||
|
payloadJson: String
|
||||||
|
): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用不需要 domainName 的 EasyTier RPC 方法。
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun callJsonRpc(serviceName: String, methodName: String, payloadJson: String): String? {
|
||||||
|
return callJsonRpc(serviceName, methodName, null, payloadJson)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取最后的错误消息
|
* 获取最后的错误消息
|
||||||
* @return 错误消息字符串,如果没有错误则返回 null
|
* @return 错误消息字符串,如果没有错误则返回 null
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::{CStr, c_char},
|
||||||
|
ptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use easytier_ffi::{call_json_rpc, free_string};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JClass, JString};
|
||||||
|
use jni::sys::jstring;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{get_last_error, throw_exception},
|
||||||
|
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn call_json_rpc_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
service_name: JString,
|
||||||
|
method_name: JString,
|
||||||
|
domain_name: JString,
|
||||||
|
payload_json: JString,
|
||||||
|
) -> jstring {
|
||||||
|
let service_name_cstr = match jstring_to_cstring(&mut env, &service_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid service name: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let method_name_cstr = match jstring_to_cstring(&mut env, &method_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid method name: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let domain_name_cstr = match optional_jstring_to_cstring(&mut env, &domain_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid domain name: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let payload_json_cstr = match jstring_to_cstring(&mut env, &payload_json) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid payload JSON: {}", e));
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let domain_name_ptr = domain_name_cstr
|
||||||
|
.as_ref()
|
||||||
|
.map_or(ptr::null(), |cstr| cstr.as_ptr());
|
||||||
|
let mut response_ptr: *const c_char = ptr::null();
|
||||||
|
let result = unsafe {
|
||||||
|
call_json_rpc(
|
||||||
|
service_name_cstr.as_ptr(),
|
||||||
|
method_name_cstr.as_ptr(),
|
||||||
|
domain_name_ptr,
|
||||||
|
payload_json_cstr.as_ptr(),
|
||||||
|
&mut response_ptr,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
if response_ptr.is_null() {
|
||||||
|
throw_exception(&mut env, "JSON RPC returned a null response");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = unsafe { CStr::from_ptr(response_ptr) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
free_string(response_ptr);
|
||||||
|
|
||||||
|
match env.new_string(&response) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to create JSON RPC response string");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
//! - `parseConfig(config)`: validate TOML config text.
|
//! - `parseConfig(config)`: validate TOML config text.
|
||||||
//! - `runNetworkInstance(config)`: start a local network instance.
|
//! - `runNetworkInstance(config)`: start a local network instance.
|
||||||
//! - `retainNetworkInstance(instanceNames)`: retain named instances and stop the rest.
|
//! - `retainNetworkInstance(instanceNames)`: retain named instances and stop the rest.
|
||||||
|
//! - `listInstances()`: return running instance names and IDs as JSON.
|
||||||
//! - `collectNetworkInfos()`: return running instance info as a JSON string.
|
//! - `collectNetworkInfos()`: return running instance info as a JSON string.
|
||||||
|
//! - `callJsonRpc(...)`: call an exposed EasyTier RPC service with JSON payload.
|
||||||
//!
|
//!
|
||||||
//! Config server client APIs:
|
//! Config server client APIs:
|
||||||
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
|
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
|
||||||
@@ -28,6 +30,7 @@ mod callback;
|
|||||||
mod config_server_api;
|
mod config_server_api;
|
||||||
mod data_plane_api;
|
mod data_plane_api;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod json_rpc_api;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod network_api;
|
mod network_api;
|
||||||
mod strings;
|
mod strings;
|
||||||
@@ -126,6 +129,53 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
|||||||
network_api::collect_network_infos_jni(env, class, max_length)
|
network_api::collect_network_infos_jni(env, class, max_length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List running network instance names and IDs.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.listInstances(maxLength: Int): String?`
|
||||||
|
///
|
||||||
|
/// Returns a JSON object whose keys are instance names and whose values are
|
||||||
|
/// instance ID strings. On failure this returns null and throws
|
||||||
|
/// `RuntimeException`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_listInstances(
|
||||||
|
env: JNIEnv,
|
||||||
|
class: JClass,
|
||||||
|
max_length: jint,
|
||||||
|
) -> jstring {
|
||||||
|
logger::init();
|
||||||
|
network_api::list_instances_jni(env, class, max_length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call an exposed EasyTier RPC method using protobuf JSON.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson): String?`
|
||||||
|
///
|
||||||
|
/// Instance lifecycle management RPCs are intentionally not exposed here. Use
|
||||||
|
/// the dedicated EasyTierJNI instance APIs for start/retain/delete/collect.
|
||||||
|
/// `payloadJson` must include any `instance` selector required by the target
|
||||||
|
/// RPC. On failure this returns null and throws `RuntimeException`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_callJsonRpc(
|
||||||
|
env: JNIEnv,
|
||||||
|
class: JClass,
|
||||||
|
service_name: JString,
|
||||||
|
method_name: JString,
|
||||||
|
domain_name: JString,
|
||||||
|
payload_json: JString,
|
||||||
|
) -> jstring {
|
||||||
|
logger::init();
|
||||||
|
json_rpc_api::call_json_rpc_jni(
|
||||||
|
env,
|
||||||
|
class,
|
||||||
|
service_name,
|
||||||
|
method_name,
|
||||||
|
domain_name,
|
||||||
|
payload_json,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the latest FFI/JNI error string for the calling thread.
|
/// Return the latest FFI/JNI error string for the calling thread.
|
||||||
///
|
///
|
||||||
/// Java signature:
|
/// Java signature:
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::{ffi::CStr, ptr};
|
|||||||
|
|
||||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||||
use easytier_ffi::{
|
use easytier_ffi::{
|
||||||
KeyValuePair, collect_network_infos, parse_config, retain_network_instance,
|
KeyValuePair, collect_network_infos, free_string, list_instance, parse_config,
|
||||||
run_network_instance, set_tun_fd,
|
retain_network_instance, run_network_instance, set_tun_fd,
|
||||||
};
|
};
|
||||||
use jni::JNIEnv;
|
use jni::JNIEnv;
|
||||||
use jni::objects::{JClass, JObjectArray, JString};
|
use jni::objects::{JClass, JObjectArray, JString};
|
||||||
@@ -190,16 +190,18 @@ pub(crate) fn collect_network_infos_jni(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = CStr::from_ptr(key_ptr).to_string_lossy();
|
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||||
let val = CStr::from_ptr(val_ptr).to_string_lossy();
|
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||||
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
|
free_string(key_ptr);
|
||||||
|
free_string(val_ptr);
|
||||||
|
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(&val) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
throw_exception(&mut env, "Failed to parse JSON");
|
throw_exception(&mut env, "Failed to parse JSON");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ret.map.insert(key.to_string(), value);
|
ret.map.insert(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||||
@@ -212,3 +214,48 @@ pub(crate) fn collect_network_infos_jni(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn list_instances_jni(mut env: JNIEnv, _class: JClass, max_length: jint) -> jstring {
|
||||||
|
let max_length = max_length.max(0) as usize;
|
||||||
|
let mut infos = vec![
|
||||||
|
KeyValuePair {
|
||||||
|
key: ptr::null(),
|
||||||
|
value: ptr::null(),
|
||||||
|
};
|
||||||
|
max_length
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let count = list_instance(infos.as_mut_ptr(), max_length);
|
||||||
|
if count < 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = serde_json::Map::new();
|
||||||
|
for info in infos.iter().take(count as usize) {
|
||||||
|
let key_ptr = info.key;
|
||||||
|
let val_ptr = info.value;
|
||||||
|
if key_ptr.is_null() || val_ptr.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
|
||||||
|
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
|
||||||
|
free_string(key_ptr);
|
||||||
|
free_string(val_ptr);
|
||||||
|
ret.insert(key, serde_json::Value::String(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = serde_json::Value::Object(ret).to_string();
|
||||||
|
match env.new_string(&json_str) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to create instance list JSON string");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ 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
|
process with `no_tun = true` and `bind_device = false`, then run TCP and UDP
|
||||||
ping/pong over the async data-plane API.
|
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:
|
To run only the async tests:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Native struct {
|
|||||||
lib unsafe.Pointer
|
lib unsafe.Pointer
|
||||||
|
|
||||||
runNetworkInstance symCall
|
runNetworkInstance symCall
|
||||||
|
callJSONRPC symCall
|
||||||
getErrorMsg symCall
|
getErrorMsg symCall
|
||||||
freeString symCall
|
freeString symCall
|
||||||
tcpConnect symCall
|
tcpConnect symCall
|
||||||
@@ -99,6 +100,48 @@ func (n *Native) RunNetworkInstance(config string) error {
|
|||||||
return nil
|
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) {
|
func (n *Native) DialContext(ctx context.Context, instance, network, address string) (net.Conn, error) {
|
||||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||||
return nil, net.UnknownNetworkError(network)
|
return nil, net.UnknownNetworkError(network)
|
||||||
@@ -219,6 +262,7 @@ func (l *Listener) Addr() net.Addr { return l.addr }
|
|||||||
func (n *Native) bind() error {
|
func (n *Native) bind() error {
|
||||||
return errors.Join(
|
return errors.Join(
|
||||||
n.bindSym(&n.runNetworkInstance, "run_network_instance", types.SInt32TypeDescriptor, types.PointerTypeDescriptor),
|
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.getErrorMsg, "get_error_msg", types.VoidTypeDescriptor, types.PointerTypeDescriptor),
|
||||||
n.bindSym(&n.freeString, "free_string", 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.tcpConnect, "data_plane_tcp_connect", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||||
|
|||||||
@@ -199,11 +199,18 @@ impl ManagedConfigServerClientHooks {
|
|||||||
let Some(callback) = self.callback else {
|
let Some(callback) = self.callback else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
let instance_name = INSTANCE_MANAGER
|
||||||
|
.get_instance_name(&instance_id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let network_name = INSTANCE_MANAGER
|
||||||
|
.get_network_name(&instance_id)
|
||||||
|
.unwrap_or_default();
|
||||||
let event_json = serde_json::json!({
|
let event_json = serde_json::json!({
|
||||||
"event": event,
|
"event": event,
|
||||||
"success": true,
|
"success": true,
|
||||||
"instance_id": instance_id.to_string(),
|
"instance_id": instance_id.to_string(),
|
||||||
|
"instance_name": instance_name,
|
||||||
|
"network_name": network_name,
|
||||||
"error": null,
|
"error": null,
|
||||||
})
|
})
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::ffi::{c_char, c_int};
|
use std::ffi::{CString, c_char, c_int};
|
||||||
|
|
||||||
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
|
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
|
||||||
|
|
||||||
@@ -301,3 +301,66 @@ pub(crate) unsafe fn collect_network_infos(
|
|||||||
|
|
||||||
index as std::ffi::c_int
|
index as std::ffi::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// List the instance names and IDs known by the FFI instance manager.
|
||||||
|
pub(crate) unsafe fn list_instance(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot list instances from config server callback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_length == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if infos.is_null() {
|
||||||
|
set_error_msg("infos is null");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let infos = unsafe { std::slice::from_raw_parts_mut(infos, max_length) };
|
||||||
|
let mut instances = INSTANCE_MANAGER
|
||||||
|
.list_network_instance_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|id| {
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.get_instance_name(&id)
|
||||||
|
.map(|name| (name, id))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
instances.sort_by(|(left_name, left_id), (right_name, right_id)| {
|
||||||
|
left_name
|
||||||
|
.cmp(right_name)
|
||||||
|
.then_with(|| left_id.to_string().cmp(&right_id.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let encoded_instances = match instances
|
||||||
|
.into_iter()
|
||||||
|
.take(max_length)
|
||||||
|
.map(|(name, id)| {
|
||||||
|
let key = CString::new(name)
|
||||||
|
.map_err(|err| format!("failed to encode instance name: {}", err))?;
|
||||||
|
let value = CString::new(id.to_string())
|
||||||
|
.map_err(|err| format!("failed to encode instance id: {}", err))?;
|
||||||
|
Ok((key, value))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()
|
||||||
|
{
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let count = encoded_instances.len();
|
||||||
|
for (index, (key, value)) in encoded_instances.into_iter().enumerate() {
|
||||||
|
infos[index] = KeyValuePair {
|
||||||
|
key: key.into_raw(),
|
||||||
|
value: value.into_raw(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
count as std::ffi::c_int
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use std::ffi::{CString, c_char, c_int};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config_server::in_config_server_callback,
|
||||||
|
error::set_error_msg,
|
||||||
|
state::{ASYNC_RUNTIME, INSTANCE_MANAGER},
|
||||||
|
strings::{c_str_to_string, optional_c_str_to_string},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// See `crate::call_json_rpc`.
|
||||||
|
pub(crate) unsafe fn call_json_rpc(
|
||||||
|
service_name: *const c_char,
|
||||||
|
method_name: *const c_char,
|
||||||
|
domain_name: *const c_char,
|
||||||
|
payload_json: *const c_char,
|
||||||
|
out_response_json: *mut *const c_char,
|
||||||
|
) -> c_int {
|
||||||
|
if out_response_json.is_null() {
|
||||||
|
set_error_msg("out_response_json is null");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
*out_response_json = std::ptr::null();
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot call JSON RPC from config server callback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let service_name = match unsafe { c_str_to_string(service_name, "service_name") } {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let method_name = match unsafe { c_str_to_string(method_name, "method_name") } {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let domain_name = match unsafe { optional_c_str_to_string(domain_name, "domain_name") } {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let payload_json = match unsafe { c_str_to_string(payload_json, "payload_json") } {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let payload = match serde_json::from_str::<serde_json::Value>(&payload_json) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to parse payload_json: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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_json = match serde_json::to_string(&response) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to serialize RPC response: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response_json = match CString::new(response_json) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to allocate RPC response: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*out_response_json = response_json.into_raw();
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
@@ -9,8 +9,10 @@
|
|||||||
//! - `run_network_instance`: start one local network instance from TOML.
|
//! - `run_network_instance`: start one local network instance from TOML.
|
||||||
//! - `retain_network_instance`: keep named instances and stop all others.
|
//! - `retain_network_instance`: keep named instances and stop all others.
|
||||||
//! - `delete_network_instance`: stop named local network instances.
|
//! - `delete_network_instance`: stop named local network instances.
|
||||||
|
//! - `list_instance`: list running instance names and IDs.
|
||||||
//! - `collect_network_infos`: collect running instance info as key/value pairs.
|
//! - `collect_network_infos`: collect running instance info as key/value pairs.
|
||||||
//! - `set_tun_fd`: attach a TUN file descriptor to a named instance.
|
//! - `set_tun_fd`: attach a TUN file descriptor to a named instance.
|
||||||
|
//! - `call_json_rpc`: call an exposed EasyTier RPC service with JSON payload.
|
||||||
//!
|
//!
|
||||||
//! Config server client APIs:
|
//! Config server client APIs:
|
||||||
//! - `start_config_server_client`: start the managed remote config client.
|
//! - `start_config_server_client`: start the managed remote config client.
|
||||||
@@ -42,6 +44,7 @@ mod data_plane;
|
|||||||
mod data_plane_async;
|
mod data_plane_async;
|
||||||
mod error;
|
mod error;
|
||||||
mod instance_api;
|
mod instance_api;
|
||||||
|
mod json_rpc;
|
||||||
mod state;
|
mod state;
|
||||||
mod strings;
|
mod strings;
|
||||||
mod types;
|
mod types;
|
||||||
@@ -140,6 +143,27 @@ pub unsafe extern "C" fn delete_network_instance(
|
|||||||
unsafe { instance_api::delete_network_instance(inst_names, length) }
|
unsafe { instance_api::delete_network_instance(inst_names, length) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List running network instance names and IDs.
|
||||||
|
///
|
||||||
|
/// Writes up to `max_length` entries into `infos`. Each returned key is the
|
||||||
|
/// instance name and each returned value is the instance ID string. Returned
|
||||||
|
/// key/value strings are allocated by this library and must be released with
|
||||||
|
/// `free_string`.
|
||||||
|
///
|
||||||
|
/// This API fails if called from a config-server event callback.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// If `max_length > 0`, `infos` must be a non-null pointer to writable storage
|
||||||
|
/// for at least `max_length` `KeyValuePair` values.
|
||||||
|
///
|
||||||
|
/// # Return
|
||||||
|
/// Returns the number of entries written, or `-1` on failure. On failure, call
|
||||||
|
/// `get_error_msg` on the same thread to retrieve details.
|
||||||
|
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||||
|
pub unsafe extern "C" fn list_instance(infos: *mut KeyValuePair, max_length: usize) -> c_int {
|
||||||
|
unsafe { instance_api::list_instance(infos, max_length) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect running network instance information.
|
/// Collect running network instance information.
|
||||||
///
|
///
|
||||||
/// Writes up to `max_length` entries into `infos`. Each returned key is the
|
/// Writes up to `max_length` entries into `infos`. Each returned key is the
|
||||||
@@ -181,6 +205,50 @@ pub unsafe extern "C" fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_in
|
|||||||
unsafe { instance_api::set_tun_fd(inst_name, fd) }
|
unsafe { instance_api::set_tun_fd(inst_name, fd) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call an exposed EasyTier RPC method using protobuf JSON.
|
||||||
|
///
|
||||||
|
/// This generic bridge intentionally excludes instance lifecycle management
|
||||||
|
/// RPCs. Use the dedicated FFI APIs for starting, retaining, deleting, and
|
||||||
|
/// collecting instances. `payload_json` must contain the protobuf JSON request,
|
||||||
|
/// including any `instance` selector required by the target RPC.
|
||||||
|
///
|
||||||
|
/// `domain_name` may be null or empty. It is only used by
|
||||||
|
/// `api.instance.TcpProxyRpcService`; null or empty defaults to `tcp`, and the
|
||||||
|
/// only accepted explicit values are `tcp`, `kcp_src`, `kcp_dst`, `quic_src`,
|
||||||
|
/// and `quic_dst`.
|
||||||
|
///
|
||||||
|
/// On success, writes a newly allocated JSON response string to
|
||||||
|
/// `out_response_json`. The caller must release it with `free_string`.
|
||||||
|
///
|
||||||
|
/// This API fails if called from a config-server event callback.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `service_name`, `method_name`, `payload_json`, and `out_response_json` must
|
||||||
|
/// be non-null. String pointers must point to null-terminated UTF-8 strings.
|
||||||
|
/// `domain_name` may be null.
|
||||||
|
///
|
||||||
|
/// # Return
|
||||||
|
/// Returns `0` on success, or `-1` on failure. On failure, call
|
||||||
|
/// `get_error_msg` on the same thread to retrieve details.
|
||||||
|
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||||
|
pub unsafe extern "C" fn call_json_rpc(
|
||||||
|
service_name: *const c_char,
|
||||||
|
method_name: *const c_char,
|
||||||
|
domain_name: *const c_char,
|
||||||
|
payload_json: *const c_char,
|
||||||
|
out_response_json: *mut *const c_char,
|
||||||
|
) -> c_int {
|
||||||
|
unsafe {
|
||||||
|
json_rpc::call_json_rpc(
|
||||||
|
service_name,
|
||||||
|
method_name,
|
||||||
|
domain_name,
|
||||||
|
payload_json,
|
||||||
|
out_response_json,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Config Server Client API =====
|
// ===== Config Server Client API =====
|
||||||
|
|
||||||
/// Start the managed config-server client.
|
/// Start the managed config-server client.
|
||||||
|
|||||||
@@ -73,6 +73,181 @@ unsafe extern "C" fn record_config_server_event(event_json: *const c_char, user_
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn take_last_error() -> Option<String> {
|
||||||
|
unsafe {
|
||||||
|
let mut error_ptr: *const c_char = std::ptr::null();
|
||||||
|
get_error_msg(&mut error_ptr);
|
||||||
|
if error_ptr.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let error = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
|
||||||
|
free_string(error_ptr);
|
||||||
|
Some(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn free_key_value_pairs(infos: &[KeyValuePair]) {
|
||||||
|
for info in infos {
|
||||||
|
free_string(info.key);
|
||||||
|
free_string(info.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_instance_returns_instance_names_and_ids() {
|
||||||
|
let instance_id = Uuid::new_v4();
|
||||||
|
let instance_name = format!("list-instance-{}", instance_id);
|
||||||
|
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)
|
||||||
|
.unwrap();
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(instance_name.clone(), instance_id);
|
||||||
|
|
||||||
|
let mut infos = vec![
|
||||||
|
KeyValuePair {
|
||||||
|
key: std::ptr::null(),
|
||||||
|
value: std::ptr::null(),
|
||||||
|
};
|
||||||
|
16
|
||||||
|
];
|
||||||
|
let count = unsafe { list_instance(infos.as_mut_ptr(), infos.len()) };
|
||||||
|
assert!(count > 0);
|
||||||
|
|
||||||
|
let mut found = false;
|
||||||
|
for info in infos.iter().take(count as usize) {
|
||||||
|
let key = unsafe { CStr::from_ptr(info.key) }.to_string_lossy();
|
||||||
|
let value = unsafe { CStr::from_ptr(info.value) }.to_string_lossy();
|
||||||
|
if key == instance_name {
|
||||||
|
assert_eq!(value, instance_id.to_string());
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free_key_value_pairs(&infos[..count as usize]);
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.delete_network_instance(vec![instance_id])
|
||||||
|
.unwrap();
|
||||||
|
remove_instance_name_ids(&[instance_id]);
|
||||||
|
assert!(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_instance_allows_zero_length() {
|
||||||
|
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_instance_rejects_null_output_pointer() {
|
||||||
|
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 1) }, -1);
|
||||||
|
assert!(take_last_error().unwrap().contains("infos is null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_json_rpc_returns_logger_response() {
|
||||||
|
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||||
|
let method = CString::new("get_logger_config").unwrap();
|
||||||
|
let payload = CString::new("{}").unwrap();
|
||||||
|
let mut response_ptr: *const c_char = std::ptr::null();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
call_json_rpc(
|
||||||
|
service.as_ptr(),
|
||||||
|
method.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
payload.as_ptr(),
|
||||||
|
&mut response_ptr,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert!(!response_ptr.is_null());
|
||||||
|
let response = unsafe { CStr::from_ptr(response_ptr) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
free_string(response_ptr);
|
||||||
|
let response: Value = serde_json::from_str(&response).unwrap();
|
||||||
|
assert!(response.get("level").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_json_rpc_rejects_instance_management_service() {
|
||||||
|
let service = CString::new("api.manage.WebClientService").unwrap();
|
||||||
|
let method = CString::new("list_network_instance").unwrap();
|
||||||
|
let payload = CString::new("{}").unwrap();
|
||||||
|
let mut response_ptr: *const c_char = std::ptr::null();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
call_json_rpc(
|
||||||
|
service.as_ptr(),
|
||||||
|
method.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
payload.as_ptr(),
|
||||||
|
&mut response_ptr,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert!(response_ptr.is_null());
|
||||||
|
assert!(take_last_error().unwrap().contains("not exposed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_json_rpc_rejects_malformed_payload_json() {
|
||||||
|
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||||
|
let method = CString::new("get_logger_config").unwrap();
|
||||||
|
let payload = CString::new("{").unwrap();
|
||||||
|
let mut response_ptr: *const c_char = std::ptr::null();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
call_json_rpc(
|
||||||
|
service.as_ptr(),
|
||||||
|
method.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
payload.as_ptr(),
|
||||||
|
&mut response_ptr,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert!(response_ptr.is_null());
|
||||||
|
assert!(
|
||||||
|
take_last_error()
|
||||||
|
.unwrap()
|
||||||
|
.contains("failed to parse payload_json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_json_rpc_rejects_null_output_pointer() {
|
||||||
|
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||||
|
let method = CString::new("get_logger_config").unwrap();
|
||||||
|
let payload = CString::new("{}").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
call_json_rpc(
|
||||||
|
service.as_ptr(),
|
||||||
|
method.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
payload.as_ptr(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
take_last_error()
|
||||||
|
.unwrap()
|
||||||
|
.contains("out_response_json is null")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn config_server_hooks_emit_run_event() {
|
async fn config_server_hooks_emit_run_event() {
|
||||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
@@ -395,10 +570,28 @@ async fn config_server_hooks_suppress_late_run_events_while_stopping() {
|
|||||||
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||||
assert_eq!(is_config_server_client_connected(), 0);
|
assert_eq!(is_config_server_client_connected(), 0);
|
||||||
|
let service = CString::new("api.logger.LoggerRpcService").unwrap();
|
||||||
|
let method = CString::new("get_logger_config").unwrap();
|
||||||
|
let payload = CString::new("{}").unwrap();
|
||||||
|
let mut response_ptr: *const c_char = std::ptr::null();
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
call_json_rpc(
|
||||||
|
service.as_ptr(),
|
||||||
|
method.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
payload.as_ptr(),
|
||||||
|
&mut response_ptr,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert!(response_ptr.is_null());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
|
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
|
||||||
-1
|
-1
|
||||||
);
|
);
|
||||||
|
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, -1);
|
||||||
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
|
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
|
||||||
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
|
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
|
||||||
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
|
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
instance_manager::NetworkInstanceManager,
|
||||||
|
proto::{
|
||||||
|
api::{
|
||||||
|
config::ConfigRpc,
|
||||||
|
instance::{
|
||||||
|
AclManageRpc, ConnectorManageRpc, CredentialManageRpc, MappedListenerManageRpc,
|
||||||
|
PeerManageRpc, PortForwardManageRpc, StatsRpc, TcpProxyRpc, VpnPortalRpc,
|
||||||
|
},
|
||||||
|
logger::LoggerRpc,
|
||||||
|
},
|
||||||
|
peer_rpc::PeerCenterRpc,
|
||||||
|
rpc_types::{
|
||||||
|
controller::BaseController,
|
||||||
|
error::{Error, Result},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rpc_service::{
|
||||||
|
acl_manage::AclManageRpcService, config::ConfigRpcService,
|
||||||
|
connector_manage::ConnectorManageRpcService, credential_manage::CredentialManageRpcService,
|
||||||
|
logger::LoggerRpcService, mapped_listener_manage::MappedListenerManageRpcService,
|
||||||
|
peer_center::PeerCenterManageRpcService, peer_manage::PeerManageRpcService,
|
||||||
|
port_forward_manage::PortForwardManageRpcService, proxy::TcpProxyRpcService,
|
||||||
|
stats::StatsRpcService, vpn_portal::VpnPortalRpcService,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const INSTANCE_MANAGEMENT_SERVICE: &str = "api.manage.WebClientService";
|
||||||
|
|
||||||
|
fn service_not_exposed(service_name: &str) -> Error {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"service {} is not exposed through FFI/JNI generic RPC",
|
||||||
|
service_name
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tcp_proxy_domain(domain_name: Option<&str>) -> Result<&'static str> {
|
||||||
|
match domain_name {
|
||||||
|
None | Some("") => Ok("tcp"),
|
||||||
|
Some("tcp") => Ok("tcp"),
|
||||||
|
Some("kcp_src") => Ok("kcp_src"),
|
||||||
|
Some("kcp_dst") => Ok("kcp_dst"),
|
||||||
|
Some("quic_src") => Ok("quic_src"),
|
||||||
|
Some("quic_dst") => Ok("quic_dst"),
|
||||||
|
Some(domain) => {
|
||||||
|
Err(anyhow::anyhow!("invalid TcpProxyRpcService domain_name: {}", domain).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn call_json_rpc(
|
||||||
|
instance_manager: &Arc<NetworkInstanceManager>,
|
||||||
|
service_name: &str,
|
||||||
|
method_name: &str,
|
||||||
|
domain_name: Option<&str>,
|
||||||
|
payload: serde_json::Value,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let ctrl = BaseController::default();
|
||||||
|
|
||||||
|
match service_name {
|
||||||
|
INSTANCE_MANAGEMENT_SERVICE => Err(service_not_exposed(service_name)),
|
||||||
|
"api.instance.PeerManageRpcService" => {
|
||||||
|
PeerManageRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.PeerCenterManageRpcService" => {
|
||||||
|
PeerCenterManageRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.ConnectorManageRpcService" => {
|
||||||
|
ConnectorManageRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.MappedListenerManageRpcService" => {
|
||||||
|
MappedListenerManageRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.VpnPortalRpcService" => {
|
||||||
|
VpnPortalRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.TcpProxyRpcService" => {
|
||||||
|
TcpProxyRpcService::new(instance_manager.clone(), tcp_proxy_domain(domain_name)?)
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.AclManageRpcService" => {
|
||||||
|
AclManageRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.PortForwardManageRpcService" => {
|
||||||
|
PortForwardManageRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.StatsRpcService" => {
|
||||||
|
StatsRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.instance.CredentialManageRpcService" => {
|
||||||
|
CredentialManageRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.logger.LoggerRpcService" => {
|
||||||
|
LoggerRpcService
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"api.config.ConfigRpcService" => {
|
||||||
|
ConfigRpcService::new(instance_manager.clone())
|
||||||
|
.json_call_method(ctrl, method_name, payload)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => Err(Error::InvalidServiceKey(
|
||||||
|
service_name.to_string(),
|
||||||
|
service_name.to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn manager() -> Arc<NetworkInstanceManager> {
|
||||||
|
Arc::new(NetworkInstanceManager::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn logger_json_rpc_succeeds() {
|
||||||
|
let response = call_json_rpc(
|
||||||
|
&manager(),
|
||||||
|
"api.logger.LoggerRpcService",
|
||||||
|
"get_logger_config",
|
||||||
|
None,
|
||||||
|
serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(response.get("level").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_rpc_rejects_unknown_service() {
|
||||||
|
let err = call_json_rpc(
|
||||||
|
&manager(),
|
||||||
|
"api.unknown.Service",
|
||||||
|
"get_logger_config",
|
||||||
|
None,
|
||||||
|
serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(err, Error::InvalidServiceKey(_, _)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_rpc_rejects_instance_management_service() {
|
||||||
|
let err = call_json_rpc(
|
||||||
|
&manager(),
|
||||||
|
INSTANCE_MANAGEMENT_SERVICE,
|
||||||
|
"list_network_instance",
|
||||||
|
None,
|
||||||
|
serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("not exposed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_rpc_rejects_unknown_method() {
|
||||||
|
let err = call_json_rpc(
|
||||||
|
&manager(),
|
||||||
|
"api.logger.LoggerRpcService",
|
||||||
|
"missing_method",
|
||||||
|
None,
|
||||||
|
serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(err, Error::InvalidMethodIndex(0, _)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_rpc_rejects_invalid_payload() {
|
||||||
|
let err = call_json_rpc(
|
||||||
|
&manager(),
|
||||||
|
"api.logger.LoggerRpcService",
|
||||||
|
"get_logger_config",
|
||||||
|
None,
|
||||||
|
serde_json::json!([]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(err, Error::MalformatRpcPacket(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_rpc_rejects_invalid_tcp_proxy_domain() {
|
||||||
|
let err = call_json_rpc(
|
||||||
|
&manager(),
|
||||||
|
"api.instance.TcpProxyRpcService",
|
||||||
|
"list_tcp_proxy_entry",
|
||||||
|
Some("bad"),
|
||||||
|
serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("invalid TcpProxyRpcService domain_name")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ mod acl_manage;
|
|||||||
mod config;
|
mod config;
|
||||||
mod connector_manage;
|
mod connector_manage;
|
||||||
mod credential_manage;
|
mod credential_manage;
|
||||||
|
mod json_rpc;
|
||||||
mod mapped_listener_manage;
|
mod mapped_listener_manage;
|
||||||
mod peer_center;
|
mod peer_center;
|
||||||
mod peer_manage;
|
mod peer_manage;
|
||||||
@@ -17,6 +18,7 @@ pub mod logger;
|
|||||||
pub mod remote_client;
|
pub mod remote_client;
|
||||||
|
|
||||||
pub type ApiRpcServer<T> = self::api::ApiRpcServer<T>;
|
pub type ApiRpcServer<T> = self::api::ApiRpcServer<T>;
|
||||||
|
pub use json_rpc::call_json_rpc;
|
||||||
|
|
||||||
pub trait InstanceRpcService: Sync + Send {
|
pub trait InstanceRpcService: Sync + Send {
|
||||||
fn get_peer_manage_service(
|
fn get_peer_manage_service(
|
||||||
|
|||||||
Reference in New Issue
Block a user