Add FFI JNI JSON RPC bridge (#2326)

* Add FFI JNI JSON RPC bridge
* Add FFI instance list API
This commit is contained in:
KKRainbow
2026-06-07 17:48:58 +08:00
committed by GitHub
parent da28c8badc
commit 9d965cae64
14 changed files with 965 additions and 9 deletions
@@ -8,6 +8,7 @@
- 📱 原生 Android JNI 支持
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
- 🛡️ 类型安全的 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 服务集成
如果您要在 Android VPN 服务中使用:
@@ -264,4 +279,4 @@ public class EasyTierVpnService extends VpnService {
- [EasyTier 主项目](https://github.com/EasyTier/EasyTier)
- [Android NDK 文档](https://developer.android.com/ndk)
- [Rust JNI 文档](https://docs.rs/jni/)
- [Rust JNI 文档](https://docs.rs/jni/)
@@ -81,6 +81,43 @@ object EasyTierJNI {
*/
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
/**
* 列出当前运行的实例名称和实例 ID。
* @param maxLength 最大返回条目数
* @return JSON 对象,key 为 instance namevalue 为 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
@@ -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.
//! - `runNetworkInstance(config)`: start a local network instance.
//! - `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.
//! - `callJsonRpc(...)`: call an exposed EasyTier RPC service with JSON payload.
//!
//! Config server client APIs:
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
@@ -28,6 +30,7 @@ mod callback;
mod config_server_api;
mod data_plane_api;
mod error;
mod json_rpc_api;
mod logger;
mod network_api;
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)
}
/// 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.
///
/// Java signature:
@@ -2,8 +2,8 @@ use std::{ffi::CStr, ptr};
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
use easytier_ffi::{
KeyValuePair, collect_network_infos, parse_config, retain_network_instance,
run_network_instance, set_tun_fd,
KeyValuePair, collect_network_infos, free_string, list_instance, parse_config,
retain_network_instance, run_network_instance, set_tun_fd,
};
use jni::JNIEnv;
use jni::objects::{JClass, JObjectArray, JString};
@@ -190,16 +190,18 @@ pub(crate) fn collect_network_infos_jni(
break;
}
let key = CStr::from_ptr(key_ptr).to_string_lossy();
let val = CStr::from_ptr(val_ptr).to_string_lossy();
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
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);
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(&val) {
Ok(v) => v,
Err(_) => {
throw_exception(&mut env, "Failed to parse JSON");
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());
@@ -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
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
@@ -24,6 +24,7 @@ type Native struct {
lib unsafe.Pointer
runNetworkInstance symCall
callJSONRPC symCall
getErrorMsg symCall
freeString symCall
tcpConnect symCall
@@ -99,6 +100,48 @@ func (n *Native) RunNetworkInstance(config string) error {
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)
@@ -219,6 +262,7 @@ 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),
@@ -199,11 +199,18 @@ impl ManagedConfigServerClientHooks {
let Some(callback) = self.callback else {
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!({
"event": event,
"success": true,
"instance_id": instance_id.to_string(),
"instance_name": instance_name,
"network_name": network_name,
"error": null,
})
.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};
@@ -301,3 +301,66 @@ pub(crate) unsafe fn collect_network_infos(
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
}
+68
View File
@@ -9,8 +9,10 @@
//! - `run_network_instance`: start one local network instance from TOML.
//! - `retain_network_instance`: keep named instances and stop all others.
//! - `delete_network_instance`: stop named local network instances.
//! - `list_instance`: list running instance names and IDs.
//! - `collect_network_infos`: collect running instance info as key/value pairs.
//! - `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:
//! - `start_config_server_client`: start the managed remote config client.
@@ -42,6 +44,7 @@ mod data_plane;
mod data_plane_async;
mod error;
mod instance_api;
mod json_rpc;
mod state;
mod strings;
mod types;
@@ -140,6 +143,27 @@ pub unsafe extern "C" fn delete_network_instance(
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.
///
/// 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) }
}
/// 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 =====
/// Start the managed config-server client.
+193
View File
@@ -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]
async fn config_server_hooks_emit_run_event() {
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() {
let _callback_scope = ConfigServerCallbackScope::enter();
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!(
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
-1
);
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, -1);
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
+232
View File
@@ -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
View File
@@ -2,6 +2,7 @@ mod acl_manage;
mod config;
mod connector_manage;
mod credential_manage;
mod json_rpc;
mod mapped_listener_manage;
mod peer_center;
mod peer_manage;
@@ -17,6 +18,7 @@ pub mod logger;
pub mod remote_client;
pub type ApiRpcServer<T> = self::api::ApiRpcServer<T>;
pub use json_rpc::call_json_rpc;
pub trait InstanceRpcService: Sync + Send {
fn get_peer_manage_service(