This guide explains how to create custom plugins for FerroTunnel.
Plugins extend FerroTunnel's functionality through hooks that intercept HTTP requests and responses.
Request → [Auth Plugin] → [Rate Limit] → [Your Plugin] → Local Service
↓
Response ← [Logger Plugin] ← [Your Plugin] ← Local Service
use ferrotunnel_plugin::{Plugin, PluginAction, RequestContext, ResponseContext};
use async_trait::async_trait;
#[derive(Default)]
pub struct MyPlugin {
// Your plugin state
}
#[async_trait]
impl Plugin for MyPlugin {
fn name(&self) -> &str {
"my-plugin"
}
async fn on_request(&self, ctx: &mut RequestContext<'_>) -> PluginAction {
// Inspect or modify the request
if ctx.request.uri().path().starts_with("/blocked") {
return PluginAction::Reject {
status: http::StatusCode::FORBIDDEN,
body: Some("Blocked by plugin".into()),
};
}
PluginAction::Continue
}
async fn on_response(&self, ctx: &mut ResponseContext<'_>) -> PluginAction {
// Inspect or modify the response
PluginAction::Continue
}
}use ferrotunnel::Server;
let server = Server::builder()
.bind("0.0.0.0:7835".parse().unwrap())
.token("secret")
.plugin(Box::new(MyPlugin::default()))
.build()?;| Action | Purpose |
|---|---|
Continue |
Pass to next plugin |
Reject { status, body } |
Reject with HTTP error |
Respond { response } |
Short-circuit with custom response |
Modify |
Request/response was modified, continue |
| Plugin | Purpose |
|---|---|
LoggerPlugin |
Logs request/response details |
TokenAuthPlugin |
Header-based token authentication |
RateLimitPlugin |
IP-based rate limiting |
CircuitBreakerPlugin |
Failure isolation |
- Keep plugins fast - Avoid blocking I/O in hooks
- Use async - All plugin methods are async
- Handle errors gracefully - Return
Rejecton errors - Log appropriately - Use
tracingfor observability
use std::collections::HashSet;
pub struct IpBlocklistPlugin {
blocked_ips: HashSet<String>,
}
#[async_trait]
impl Plugin for IpBlocklistPlugin {
fn name(&self) -> &str { "ip-blocklist" }
async fn on_request(&self, ctx: &mut RequestContext<'_>) -> PluginAction {
if let Some(addr) = ctx.remote_addr {
if self.blocked_ips.contains(&addr.ip().to_string()) {
return PluginAction::Reject {
status: http::StatusCode::FORBIDDEN,
body: Some("IP blocked".into()),
};
}
}
PluginAction::Continue
}
}#[tokio::test]
async fn test_my_plugin() {
let plugin = MyPlugin::default();
let mut request = http::Request::builder()
.uri("/test")
.body(())
.unwrap();
let mut ctx = RequestContext {
request: &mut request,
remote_addr: None,
};
let action = plugin.on_request(&mut ctx).await;
assert!(matches!(action, PluginAction::Continue));
}