Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ anyhow = "1"
sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "postgres", "chrono", "macros"] }
chrono = "0.4.44"
p256 = { version = "0.13.2", features = ["ecdsa", "pkcs8"] }
governor = "0.6"
dashmap = "5"

[dev-dependencies]
rcgen = { version = "0.14", features = ["x509-parser"] }
Expand Down
7 changes: 7 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ max_connections = 5
connect_timeout_ms = 5000
pool_acquire_timeout_ms = 1000
query_timeout_ms = 500
[rate_limit]
# Per-identity byte rate limit. Set to 0 to disable (default).
# Each agent identity (from the X.509 certificate extension) gets
# an independent token bucket. Limits sustained exfiltration throughput.
# Example: 10_485_760 = 10 MB/s
bytes_per_second = 0
burst_bytes = 1048576
28 changes: 27 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,38 @@ use serde::Deserialize;

use crate::policy;

/// Per-identity byte-rate limiting configuration.
/// Set `bytes_per_second = 0` to disable (default).
#[derive(Debug, Deserialize, Clone, Copy)]
pub struct RateLimitConfig {
#[serde(default)]
pub bytes_per_second: u64,
#[serde(default = "default_burst_bytes")]
pub burst_bytes: u64,
}

fn default_burst_bytes() -> u64 {
1_048_576 // 1 MB
}

impl Default for RateLimitConfig {
fn default() -> Self {
Self {
bytes_per_second: 0,
burst_bytes: default_burst_bytes(),
}
}
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
// Note: deny_unknown_fields removed from Config so the optional
// [rate_limit] section can be absent from existing config files.
pub struct Config {
pub server: ServerConfig,
pub observability: ObservabilityConfig,
pub policy: PolicyConfig,
#[serde(default)]
pub rate_limit: RateLimitConfig,
}

#[derive(Debug, Deserialize)]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ pub mod config;
pub mod observability;
pub mod policy;
pub mod proxy;
pub mod rate_limit;
mod registry;
pub mod tls;
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ use hyper_util::rt::TokioExecutor;
use tokio::net::TcpListener;
use tracing::{error, info};

mod rate_limit;

#[derive(Parser)]
#[command(name = "agent_gateway", about = "mTLS HTTP/2 CONNECT proxy")]
struct Cli {
/// Path to the TOML configuration file
#[arg(short, long, default_value = "config.toml")]
config: PathBuf,
}
Expand All @@ -37,7 +38,9 @@ async fn serve(config: config::Config) -> anyhow::Result<()> {
let tls_acceptor = tls::TlsAcceptor::from(server_tls);

let policy_engine = policy::build_engine(&config.policy).await?;
let make_service = Arc::new(MakeProxyService::new(policy_engine));

// Pass rate_limit config into the service — used in spawn_tunnel
let make_service = Arc::new(MakeProxyService::new(policy_engine, config.rate_limit));

let listen_addr: std::net::SocketAddr = config.server.listen_addr.parse()?;
let listener = TcpListener::bind(listen_addr).await?;
Expand Down
Loading