[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:
Claude
2026-03-26 11:41:38 +00:00
committed by Stan Grams
parent 9692e31c8c
commit c8f33b8939
11 changed files with 2 additions and 304 deletions
-4
View File
@@ -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
View File
@@ -2546,7 +2546,6 @@ name = "trx-app"
version = "0.1.0"
dependencies = [
"dirs",
"libloading",
"serde",
"thiserror 2.0.17",
"toml",
-10
View File
@@ -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
-18
View File
@@ -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 }
-19
View File
@@ -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.
-50
View File
@@ -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);
})
}
}
-1
View File
@@ -14,5 +14,4 @@ toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
dirs = "6"
libloading = "0.8"
thiserror = "2"
-2
View File
@@ -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;
-189
View File
@@ -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)
}
+1 -5
View File
@@ -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());
}
+1 -5
View File
@@ -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());
}