sn-exit v0: quota-controlled uplink proxy (byte relay, CONNECT, SSRF guard)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
//! CLI entry point for sn-exit.
|
||||
//!
|
||||
//! KEINE GARANTIE — this software is provided "as is" without warranty of
|
||||
//! any kind (AGPL-3.0, sections 15/16). Users are solely responsible for
|
||||
//! what they transmit or receive with it.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use sn_exit::config::{self, ExitConfig, QuotaPolicy, SourceAllowlist};
|
||||
|
||||
/// Per-neighbor quota is disabled when 0 is passed (no limit).
|
||||
const QUOTA_DISABLED: u64 = 0;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "sn-exit",
|
||||
about = "SpiderNet exit service: quota-controlled uplink proxy for neighbors"
|
||||
)]
|
||||
struct Args {
|
||||
/// Address to listen on — ideally the household's Yggdrasil address,
|
||||
/// e.g. `[200:1234::1]:8080`.
|
||||
#[arg(long, default_value = "127.0.0.1:8080")]
|
||||
bind: String,
|
||||
|
||||
/// Neighbor addresses allowed to use this exit (repeatable, e.g.
|
||||
/// `200:5678::2`). Without any `--allow`, only loopback clients are
|
||||
/// accepted (development mode).
|
||||
#[arg(long = "allow", value_delimiter = ',')]
|
||||
allow: Vec<String>,
|
||||
|
||||
/// Quota per neighbor per window, in MiB. `0` disables the quota.
|
||||
#[arg(long, default_value_t = 1024)]
|
||||
quota_mib: u64,
|
||||
|
||||
/// Quota window in hours (fixed window per neighbor).
|
||||
#[arg(long, default_value_t = 24)]
|
||||
window_hours: u64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
if let Err(report) = run().await {
|
||||
eprintln!("NO WARRANTY — SpiderNet sn-exit (AGPL-3.0, no liability)");
|
||||
eprintln!("error: {report}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), String> {
|
||||
let args = Args::parse();
|
||||
|
||||
let bind = config::parse_bind(&args.bind).map_err(|err| err.to_string())?;
|
||||
let allowlist = if args.allow.is_empty() {
|
||||
SourceAllowlist::loopback_only()
|
||||
} else {
|
||||
let sources = args
|
||||
.allow
|
||||
.iter()
|
||||
.map(|addr| config::parse_source(addr).map_err(|err| err.to_string()))
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
SourceAllowlist::new(sources)
|
||||
};
|
||||
let quota = if args.quota_mib == QUOTA_DISABLED {
|
||||
None
|
||||
} else {
|
||||
let max_bytes = args.quota_mib.saturating_mul(1024 * 1024);
|
||||
Some(
|
||||
QuotaPolicy::try_new(max_bytes, Duration::from_secs(args.window_hours * 3600))
|
||||
.map_err(|err| err.to_string())?,
|
||||
)
|
||||
};
|
||||
|
||||
let config = ExitConfig::new(bind, allowlist, quota);
|
||||
let listener = tokio::net::TcpListener::bind(config.bind())
|
||||
.await
|
||||
.map_err(|err| format!("cannot bind {}: {err}", config.bind()))?;
|
||||
|
||||
println!("NO WARRANTY — SpiderNet sn-exit (AGPL-3.0, no liability)");
|
||||
println!();
|
||||
if config.allowlist().is_loopback_only() {
|
||||
println!("DEV MODE: no --allow given — only loopback clients are accepted.");
|
||||
}
|
||||
match config.quota() {
|
||||
Some(policy) => println!(
|
||||
"quota: {} MiB per {}h per neighbor (enforced at connection start)",
|
||||
policy.max_bytes() / (1024 * 1024),
|
||||
policy.window().as_secs() / 3600,
|
||||
),
|
||||
None => println!("quota: disabled — neighbors may use unlimited uplink traffic"),
|
||||
}
|
||||
println!(
|
||||
"sharing: this household now offers its uplink (stop sn-exit to revoke = kill-switch)"
|
||||
);
|
||||
println!("listen: {}", config.bind());
|
||||
|
||||
sn_exit::proxy::serve(config, listener)
|
||||
.await
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user