[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`.
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
|
||||
Generated
-1
@@ -2546,7 +2546,6 @@ name = "trx-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"libloading",
|
||||
"serde",
|
||||
"thiserror 2.0.17",
|
||||
"toml",
|
||||
|
||||
@@ -161,16 +161,6 @@ Audio is transported as Opus between server, client, and browser.
|
||||
- `trx-client` relays audio to the HTTP frontend
|
||||
- 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
|
||||
|
||||
- [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-subscriber = { workspace = true }
|
||||
dirs = "6"
|
||||
libloading = "0.8"
|
||||
thiserror = "2"
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
|
||||
pub mod config;
|
||||
pub mod logging;
|
||||
pub mod plugins;
|
||||
pub mod util;
|
||||
|
||||
pub use config::{ConfigError, ConfigFile};
|
||||
pub use logging::init_logging;
|
||||
pub use plugins::{load_backend_plugins, load_frontend_plugins};
|
||||
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::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::NonNull;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
@@ -20,7 +19,7 @@ use tokio::sync::{broadcast, mpsc, watch};
|
||||
use tokio::task::JoinHandle;
|
||||
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::decode::DecodedMessage;
|
||||
@@ -147,9 +146,6 @@ async fn async_init() -> DynResult<AppState> {
|
||||
|
||||
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 {
|
||||
info!("Loaded configuration from {}", path.display());
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -27,7 +26,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
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_core::rig::controller::{AdaptivePolling, ExponentialBackoff};
|
||||
use trx_core::rig::request::RigRequest;
|
||||
@@ -854,9 +853,6 @@ async fn main() -> DynResult<()> {
|
||||
|
||||
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 {
|
||||
info!("Loaded configuration from {}", path.display());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user