[refactor](trx-rs): remove shared-library plugin system
Drop the plugin loading infrastructure (libloading-based dynamic .so/.dylib/.dll loading) from both trx-server and trx-client. The feature was unused and posed an unnecessary security risk by executing arbitrary native code from disk. Removed: - src/trx-app/src/plugins.rs (plugin discovery, validation, FFI registration) - examples/trx-plugin-example/ (cdylib example plugin) - libloading dependency from trx-app - load_backend_plugins / load_frontend_plugins calls from server and client - Plugin documentation from README.md and CLAUDE.md https://claude.ai/code/session_01DTEUpz3XPUeWmz74NeaFgb Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -88,10 +88,6 @@ The rig controller (`src/trx-core/src/rig/controller/`) is the central state man
|
|||||||
|
|
||||||
Signal decoders run as background tasks in `trx-server`, consuming decoded audio. `trx-ftx` provides the FT8/FT4/FT2 decoder in pure Rust. Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via `trx-decode-log`.
|
Signal decoders run as background tasks in `trx-server`, consuming decoded audio. `trx-ftx` provides the FT8/FT4/FT2 decoder in pure Rust. Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via `trx-decode-log`.
|
||||||
|
|
||||||
### Plugin system
|
|
||||||
|
|
||||||
Both `trx-server` and `trx-client` can load shared-library plugins exporting a `trx_register` symbol. Search paths: `./plugins`, `~/.config/trx-rs/plugins`, `TRX_PLUGIN_DIRS` env var.
|
|
||||||
|
|
||||||
## Commit Format
|
## Commit Format
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
Generated
-1
@@ -2546,7 +2546,6 @@ name = "trx-app"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"libloading",
|
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"toml",
|
"toml",
|
||||||
|
|||||||
@@ -161,16 +161,6 @@ Audio is transported as Opus between server, client, and browser.
|
|||||||
- `trx-client` relays audio to the HTTP frontend
|
- `trx-client` relays audio to the HTTP frontend
|
||||||
- Browsers connect over `/audio`
|
- Browsers connect over `/audio`
|
||||||
|
|
||||||
## Plugins
|
|
||||||
|
|
||||||
Both binaries can discover shared-library plugins through:
|
|
||||||
|
|
||||||
- `./plugins`
|
|
||||||
- `~/.config/trx-rs/plugins`
|
|
||||||
- `TRX_PLUGIN_DIRS`
|
|
||||||
|
|
||||||
See [`examples/trx-plugin-example/README.md`](examples/trx-plugin-example/README.md).
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [User Manual](docs/User-Manual.md): configuration, features, and usage
|
- [User Manual](docs/User-Manual.md): configuration, features, and usage
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "trx-plugin-example"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
trx-backend = { path = "../../src/trx-server/trx-backend" }
|
|
||||||
trx-core = { path = "../../src/trx-core" }
|
|
||||||
trx-frontend = { path = "../../src/trx-client/trx-frontend" }
|
|
||||||
tokio = { workspace = true, features = ["full"] }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# trx-plugin-example
|
|
||||||
|
|
||||||
This is a minimal shared-library plugin that registers a backend and frontend.
|
|
||||||
The backend is a stub that returns an error; the frontend is a no-op spawner.
|
|
||||||
|
|
||||||
Build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build -p trx-plugin-example --release
|
|
||||||
```
|
|
||||||
|
|
||||||
Install (example):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p plugins
|
|
||||||
cp target/release/libtrx_plugin_example.* plugins/
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `trx-server` or `trx-client` with `TRX_PLUGIN_DIRS=./plugins` to discover the plugin.
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
use tokio::sync::{mpsc, watch};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use trx_backend::{RegistrationContext, RigAccess};
|
|
||||||
use trx_core::{DynResult, RigRequest, RigState};
|
|
||||||
use trx_frontend::{FrontendRuntimeContext, FrontendSpawner, FrontendRegistrationContext};
|
|
||||||
|
|
||||||
const BACKEND_NAME: &str = "example";
|
|
||||||
const FRONTEND_NAME: &str = "example-frontend";
|
|
||||||
|
|
||||||
/// Entry point called by trx-server when the plugin is loaded.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn trx_register_backend(context: *mut std::ffi::c_void) {
|
|
||||||
let context = unsafe { &mut *(context as *mut RegistrationContext) };
|
|
||||||
context.register_backend(BACKEND_NAME, example_backend_factory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Entry point called by trx-client when the plugin is loaded.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn trx_register_frontend(context: *mut std::ffi::c_void) {
|
|
||||||
let context = unsafe { &mut *(context as *mut FrontendRegistrationContext) };
|
|
||||||
context.register_frontend(FRONTEND_NAME, ExampleFrontend::spawn_frontend);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn example_backend_factory(_access: RigAccess) -> DynResult<Box<dyn trx_core::rig::RigCat>> {
|
|
||||||
Err("example plugin backend not implemented".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ExampleFrontend;
|
|
||||||
|
|
||||||
impl FrontendSpawner for ExampleFrontend {
|
|
||||||
fn spawn_frontend(
|
|
||||||
_state_rx: watch::Receiver<RigState>,
|
|
||||||
_rig_tx: mpsc::Sender<RigRequest>,
|
|
||||||
_callsign: Option<String>,
|
|
||||||
listen_addr: SocketAddr,
|
|
||||||
_context: std::sync::Arc<FrontendRuntimeContext>,
|
|
||||||
) -> JoinHandle<()> {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
info!("example frontend loaded at {} (no-op)", listen_addr);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,5 +14,4 @@ toml = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
libloading = "0.8"
|
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
|||||||
@@ -4,10 +4,8 @@
|
|||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod plugins;
|
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use config::{ConfigError, ConfigFile};
|
pub use config::{ConfigError, ConfigFile};
|
||||||
pub use logging::init_logging;
|
pub use logging::init_logging;
|
||||||
pub use plugins::{load_backend_plugins, load_frontend_plugins};
|
|
||||||
pub use util::normalize_name;
|
pub use util::normalize_name;
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::ptr::NonNull;
|
|
||||||
|
|
||||||
use libloading::{Library, Symbol};
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
/// Environment variable to disable plugin loading entirely.
|
|
||||||
const PLUGIN_DISABLE_ENV: &str = "TRX_PLUGINS_DISABLED";
|
|
||||||
|
|
||||||
const PLUGIN_ENV: &str = "TRX_PLUGIN_DIRS";
|
|
||||||
const BACKEND_ENTRYPOINT: &str = "trx_register_backend";
|
|
||||||
const FRONTEND_ENTRYPOINT: &str = "trx_register_frontend";
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
const PATH_SEPARATOR: char = ';';
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
const PATH_SEPARATOR: char = ':';
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
const PLUGIN_EXTENSIONS: &[&str] = &["dll"];
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
const PLUGIN_EXTENSIONS: &[&str] = &["dylib"];
|
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
|
||||||
const PLUGIN_EXTENSIONS: &[&str] = &["so"];
|
|
||||||
|
|
||||||
pub fn load_backend_plugins(context: NonNull<std::ffi::c_void>) -> Vec<Library> {
|
|
||||||
load_plugins_for_entrypoint(BACKEND_ENTRYPOINT, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_frontend_plugins(context: NonNull<std::ffi::c_void>) -> Vec<Library> {
|
|
||||||
load_plugins_for_entrypoint(FRONTEND_ENTRYPOINT, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_plugins_for_entrypoint(
|
|
||||||
entrypoint: &str,
|
|
||||||
context: NonNull<std::ffi::c_void>,
|
|
||||||
) -> Vec<Library> {
|
|
||||||
// Allow disabling plugin loading entirely via environment variable.
|
|
||||||
if std::env::var(PLUGIN_DISABLE_ENV).is_ok() {
|
|
||||||
info!("Plugin loading disabled via {}", PLUGIN_DISABLE_ENV);
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut libraries = Vec::new();
|
|
||||||
let search_paths = plugin_search_paths();
|
|
||||||
|
|
||||||
if search_paths.is_empty() {
|
|
||||||
return libraries;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Plugin search paths: {:?}", search_paths);
|
|
||||||
|
|
||||||
for path in search_paths {
|
|
||||||
if let Err(err) = load_plugins_from_dir(&path, entrypoint, context, &mut libraries) {
|
|
||||||
warn!("Plugin scan failed for {:?}: {}", path, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libraries
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_plugins_from_dir(
|
|
||||||
path: &Path,
|
|
||||||
entrypoint: &str,
|
|
||||||
context: NonNull<std::ffi::c_void>,
|
|
||||||
libraries: &mut Vec<Library>,
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in std::fs::read_dir(path)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if !path.is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !is_plugin_file(&path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file permissions before loading.
|
|
||||||
if !validate_plugin_file(&path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
match Library::new(&path) {
|
|
||||||
Ok(lib) => {
|
|
||||||
if let Err(err) = register_library(&lib, &path, entrypoint, context) {
|
|
||||||
warn!("Plugin {:?} failed to register: {}", path, err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
info!("Loaded plugin {:?}", path);
|
|
||||||
libraries.push(lib);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to load plugin {:?}: {}", path, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn register_library(
|
|
||||||
lib: &Library,
|
|
||||||
path: &Path,
|
|
||||||
entrypoint: &str,
|
|
||||||
context: NonNull<std::ffi::c_void>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let entry: Symbol<unsafe extern "C" fn(*mut std::ffi::c_void)> = lib
|
|
||||||
.get(entrypoint.as_bytes())
|
|
||||||
.map_err(|e| format!("missing entrypoint {}: {}", entrypoint, e))?;
|
|
||||||
entry(context.as_ptr());
|
|
||||||
info!("Registered plugin {:?}", path);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plugin_search_paths() -> Vec<PathBuf> {
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
|
|
||||||
if let Ok(env_paths) = std::env::var(PLUGIN_ENV) {
|
|
||||||
for raw in env_paths.split(PATH_SEPARATOR) {
|
|
||||||
if raw.trim().is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
paths.push(PathBuf::from(raw));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
paths.push(PathBuf::from("plugins"));
|
|
||||||
|
|
||||||
if let Some(config_dir) = dirs::config_dir() {
|
|
||||||
paths.push(config_dir.join("trx-rs").join("plugins"));
|
|
||||||
}
|
|
||||||
|
|
||||||
paths
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate plugin file before loading: check ownership and permissions.
|
|
||||||
///
|
|
||||||
/// On Unix, reject files that are world-writable (mode & 0o002) to prevent
|
|
||||||
/// loading tampered libraries. On non-Unix platforms, always accept.
|
|
||||||
fn validate_plugin_file(path: &Path) -> bool {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::MetadataExt;
|
|
||||||
match std::fs::metadata(path) {
|
|
||||||
Ok(meta) => {
|
|
||||||
let mode = meta.mode();
|
|
||||||
if mode & 0o002 != 0 {
|
|
||||||
warn!(
|
|
||||||
"Skipping world-writable plugin {:?} (mode {:o})",
|
|
||||||
path, mode
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Cannot stat plugin {:?}: {}", path, e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
let _ = path;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_plugin_file(path: &Path) -> bool {
|
|
||||||
path.extension()
|
|
||||||
.and_then(OsStr::to_str)
|
|
||||||
.map(|ext| {
|
|
||||||
PLUGIN_EXTENSIONS
|
|
||||||
.iter()
|
|
||||||
.any(|e| ext.eq_ignore_ascii_case(e))
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ mod remote_client;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::ptr::NonNull;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@@ -20,7 +19,7 @@ use tokio::sync::{broadcast, mpsc, watch};
|
|||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use trx_app::{init_logging, load_frontend_plugins, normalize_name};
|
use trx_app::{init_logging, normalize_name};
|
||||||
use trx_core::audio::AudioStreamInfo;
|
use trx_core::audio::AudioStreamInfo;
|
||||||
|
|
||||||
use trx_core::decode::DecodedMessage;
|
use trx_core::decode::DecodedMessage;
|
||||||
@@ -147,9 +146,6 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
|
|
||||||
init_logging(cfg.general.log_level.as_deref());
|
init_logging(cfg.general.log_level.as_deref());
|
||||||
|
|
||||||
let frontend_ctx_ptr = NonNull::from(&mut frontend_reg_ctx).cast();
|
|
||||||
let _plugin_libs = load_frontend_plugins(frontend_ctx_ptr);
|
|
||||||
|
|
||||||
if let Some(ref path) = config_path {
|
if let Some(ref path) = config_path {
|
||||||
info!("Loaded configuration from {}", path.display());
|
info!("Loaded configuration from {}", path.display());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use std::collections::HashMap;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::ptr::NonNull;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ use tracing::{error, info, warn};
|
|||||||
|
|
||||||
use trx_core::audio::AudioStreamInfo;
|
use trx_core::audio::AudioStreamInfo;
|
||||||
|
|
||||||
use trx_app::{init_logging, load_backend_plugins, normalize_name};
|
use trx_app::{init_logging, normalize_name};
|
||||||
use trx_backend::{register_builtin_backends_on, RegistrationContext, RigAccess};
|
use trx_backend::{register_builtin_backends_on, RegistrationContext, RigAccess};
|
||||||
use trx_core::rig::controller::{AdaptivePolling, ExponentialBackoff};
|
use trx_core::rig::controller::{AdaptivePolling, ExponentialBackoff};
|
||||||
use trx_core::rig::request::RigRequest;
|
use trx_core::rig::request::RigRequest;
|
||||||
@@ -854,9 +853,6 @@ async fn main() -> DynResult<()> {
|
|||||||
|
|
||||||
init_logging(cfg.general.log_level.as_deref());
|
init_logging(cfg.general.log_level.as_deref());
|
||||||
|
|
||||||
let bootstrap_ctx_ptr = NonNull::from(&mut bootstrap_ctx).cast();
|
|
||||||
let _plugin_libs = load_backend_plugins(bootstrap_ctx_ptr);
|
|
||||||
|
|
||||||
if let Some(ref path) = config_path {
|
if let Some(ref path) = config_path {
|
||||||
info!("Loaded configuration from {}", path.display());
|
info!("Loaded configuration from {}", path.display());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user