use std::net::SocketAddr; use std::process::exit; use std::sync::Arc; use std::time::Duration; use fgex_proxy::control::serve_stdin; use fgex_proxy::filter::{ChainHandle, FilterChain}; use fgex_proxy::proxy::{Proxy, ProxyConfig, TlsSetup, Upstream}; use fgex_proxy::quic::{QuicConfig, QuicManager, QuicSetup}; use fgex_proxy::relays::Relays; use fgex_proxy::spec::parse_filters; use fgex_proxy::transparent::probe_capability; /// How often the engine reports its connection counters. One short line, so the cost is /// nothing; the first is sent immediately so a freshly started service has a baseline /// rather than an unknown for the first interval. const STATS_INTERVAL: Duration = Duration::from_secs(2); fn env_or_exit(key: &str) -> String { match std::env::var(key) { Ok(v) => v, Err(_) => { eprintln!("[fatal] [main] {key} is required"); exit(2); } } } fn env_flag(key: &str) -> bool { matches!( std::env::var(key).unwrap_or_default().trim(), "1" | "true" | "yes" ) } fn parse_addr(key: &str, raw: &str) -> SocketAddr { match raw.parse() { Ok(a) => a, Err(e) => { eprintln!("[fatal] [main] {key}='{raw}' is not a valid address: {e}"); exit(2); } } } /// `FGEX_PROXY_TLS_CERT` / `_KEY` are paths, not the material itself: a certificate /// on a command line or in an env dump is a certificate in a log somewhere. fn env_usize(key: &str) -> Option { std::env::var(key).ok()?.trim().parse().ok() } /// The certificate and key this service terminates with, read once. /// /// Read rather than passed: a certificate on a command line or in an environment dump is /// a certificate in a log somewhere. Both edges that need it — TLS over TCP and QUIC — /// ask for the same pair, so it is read here and handed to whichever wants it. fn read_material() -> Result, String> { let cert_path = std::env::var("FGEX_PROXY_TLS_CERT").ok(); let key_path = std::env::var("FGEX_PROXY_TLS_KEY").ok(); match (cert_path, key_path) { (Some(cert), Some(key)) => { let certificate = std::fs::read_to_string(&cert).map_err(|e| format!("cannot read {cert}: {e}"))?; let key = std::fs::read_to_string(&key).map_err(|e| format!("cannot read the key: {e}"))?; Ok(Some((certificate, key))) } (None, None) => Ok(None), _ => Err("FGEX_PROXY_TLS_CERT and FGEX_PROXY_TLS_KEY go together".to_string()), } } fn build_tls(material: Option<&(String, String)>, optional: bool) -> Result { let server = match material { Some((cert, key)) => Some(fgex_proxy::tls::server_config(cert, key)?), None => None, }; // Built whenever this engine terminates anything, and *used* per address: whether a // given connection is re-encrypted on the way out is the address's answer // (`Onward`), not the process's. A plain TCP service terminates nothing, so it has // no client configuration and never did — which is why none of this reaches it. let upstream = match server { Some(_) => Some(fgex_proxy::tls::client_config()?), None => None, }; Ok(TlsSetup { server, upstream, optional, }) } /// Answer one regex-debug request and exit, instead of starting a datapath. /// /// Its own mode rather than a long-running service: the backend asks rarely, an /// operator's half-written pattern must not be able to affect anything that is /// carrying traffic, and a process that exits cannot leak scratch space or state /// between requests. fn debug_regex() -> ! { use std::io::Read; let mut request = String::new(); if let Err(e) = std::io::stdin().read_to_string(&mut request) { eprintln!("[fatal] [debug] cannot read the request: {e}"); exit(2); } let response = fgex_proxy::debug::run(&request); match serde_json::to_string(&response) { Ok(json) => println!("{json}"), Err(e) => { eprintln!("[fatal] [debug] cannot encode the response: {e}"); exit(2); } } exit(0); } /// How many worker threads the runtime gets. /// /// `NTHREADS` is what `run.py --threads` sets, and it used to reach only the NFQUEUE /// binaries — this engine took every core whatever the operator asked for, so the same /// flag meant one thing on one layer and nothing on the other. It means the same thing /// on both now. Absent or unparseable is "as many as there are cores", which is tokio's /// own default and the right answer when nobody has said otherwise. fn worker_threads() -> usize { std::env::var("NTHREADS") .ok() .and_then(|v| v.trim().parse::().ok()) .filter(|n| *n > 0) .unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)) } fn main() { // Built by hand rather than through `#[tokio::main]`, which offers no way to say how // many workers it should have. let runtime = match tokio::runtime::Builder::new_multi_thread() .worker_threads(worker_threads()) .enable_all() .build() { Ok(rt) => rt, Err(e) => { eprintln!("[fatal] [main] cannot start the runtime: {e}"); exit(1); } }; runtime.block_on(run()); } async fn run() { // Checked before anything else reads the environment: this mode starts no // listener, opens no sockets and touches no rules. if std::env::args().any(|a| a == "--debug-regex") { debug_regex(); } let listen_raw = env_or_exit("FGEX_PROXY_LISTEN"); let listen = parse_addr("FGEX_PROXY_LISTEN", &listen_raw); // "original" puts one listener in front of every intercepted service; a literal // address keeps the simpler one-listener-one-service shape. let upstream_raw = env_or_exit("FGEX_PROXY_UPSTREAM"); let upstream = if matches!(upstream_raw.trim(), "original" | "auto") { Upstream::Original } else { Upstream::Fixed(parse_addr("FGEX_PROXY_UPSTREAM", &upstream_raw)) }; // Only the outbound dial needs IP_TRANSPARENT now: the listener is a plain one // that traffic is redirected into. Binding a foreign address was a tproxy // requirement, and tproxy is gone. let spoof_source = env_flag("FGEX_PROXY_SPOOF_SOURCE"); // Fail here rather than per connection: a missing CAP_NET_ADMIN would otherwise // show up only as every service seeing the proxy's address instead of the client's. if spoof_source { if let Err(e) = probe_capability(listen.is_ipv6()) { eprintln!( "[fatal] [main] transparent mode needs CAP_NET_ADMIN, IP_TRANSPARENT failed: {e}" ); exit(1); } } let deadline = std::env::var("FGEX_PROXY_FILTER_TIMEOUT_MS") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(2000); let connect_timeout = std::env::var("FGEX_PROXY_CONNECT_TIMEOUT_MS") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(5000); let specs = std::env::var("FGEX_PROXY_FILTERS").unwrap_or_default(); let filters = match parse_filters(&specs) { Ok(f) => f, Err(e) => { eprintln!("[fatal] [main] bad FGEX_PROXY_FILTERS: {e}"); exit(2); } }; // Read before either edge is built: TLS over TCP and QUIC terminate with the same // certificate, and a service speaks one of them or the other. let material = match read_material() { Ok(material) => material, Err(e) => { eprintln!("[fatal] [main] {e}"); exit(2); } }; // QUIC is UDP on the wire, so it takes the place of the datagram relays rather than // sitting beside them: the same map of one port per protected address, bound by // something that terminates instead of something that forwards. let quic = env_flag("FGEX_PROXY_QUIC"); // Whether the TCP listener terminates TLS, and whether it insists on it. // // `FGEX_PROXY_TLS_OPTIONAL` is what an `http` service sets: one certificate, one // chain, and each connection carried as whatever the client opened it with — in the // clear on one port and under TLS on another. It also means TLS on TCP and QUIC on // UDP live in **one** engine process, which they could not before: the two used to be // exclusive here, on the reasoning that a service speaks one or the other, and an // HTTP service speaks both. let tls_optional = env_flag("FGEX_PROXY_TLS_OPTIONAL"); // Absent, the old rule: a certificate means terminate, unless this is a QUIC service, // where the certificate belongs to the QUIC edge and the TCP listener has nothing // pointed at it. Said explicitly by anything that wants both. let terminate_tls = env_flag("FGEX_PROXY_TLS") || tls_optional || !quic; let tls = match build_tls( if terminate_tls { material.as_ref() } else { None }, tls_optional, ) { Ok(setup) => setup, Err(e) => { eprintln!("[fatal] [main] {e}"); exit(2); } }; // Stamped on every socket this engine opens towards a service, so the rules that // intercept that service can tell our own dial from the traffic they are meant to // catch. Read once: the UDP relays need the same mark as the TCP path. let cfg_self_mark = std::env::var("FGEX_PROXY_SELF_MARK") .ok() .and_then(|v| u32::from_str_radix(v.trim_start_matches("0x"), 16).ok()) .or(Some(fgex_proxy::transparent::SELF_MARK)); // Read once and shared by both paths: TCP connections and UDP flows are the same // resource being defended, and two numbers meaning "how much of it may be spent" // would be two things to keep in step. let max_connections = env_usize("FGEX_PROXY_MAX_CONNECTIONS").unwrap_or(0); let over_limit_forwards = env_flag("FGEX_PROXY_OVER_LIMIT_FORWARD"); // In seconds, because that is the unit an operator thinks in for "how long may a // connection say nothing". Zero and absent both mean off. let first_byte_timeout = match env_usize("FGEX_PROXY_FIRST_BYTE_TIMEOUT").unwrap_or(0) { 0 => None, secs => Some(Duration::from_secs(secs as u64)), }; // Opened once for the process and shared by every path that reconstructs something: // the interface belongs to the instance, not to a service or a connection. let capture = fgex_proxy::capture::Capture::open(); // Where each published address sends its traffic, for the ones this engine starts // with. `10.0.0.1:443=10.0.0.1:80` reads as "what arrives at the first is the // service at the second" — an address published on a port the service does not // listen on. Absent, and for every address not named, the answer is the address // itself, which is what transparent means and what every service had before. let targets = std::sync::Arc::new(fgex_proxy::proxy::Targets::default()); if let Ok(spec) = std::env::var("FGEX_PROXY_TARGETS") { for entry in spec.split(',').map(str::trim).filter(|s| !s.is_empty()) { // `10.0.0.1:443|tls=10.0.0.1:80`, and every part after the address optional: // the edge says what is spoken there and the target where the service is, // and an address that says neither is one this engine is simply in front of. let (address, target) = match entry.split_once('=') { Some((address, target)) => ( address, Some(parse_addr("FGEX_PROXY_TARGETS", target.trim())), ), None => (entry, None), }; // `
||`, both words optional and both defaulting to // the answer every address gave before there was a question. let mut words = address.split('|'); let address = words.next().unwrap_or(address); let edge = match words.next() { Some("tls") => fgex_proxy::proxy::Edge::Tls, None | Some("clear") | Some("any") => fgex_proxy::proxy::Edge::Whatever, Some(other) => { eprintln!("[fatal] [main] FGEX_PROXY_TARGETS: unknown edge '{other}'"); exit(2); } }; let upstream = match words.next() { Some("plain") => fgex_proxy::proxy::Onward::Plain, Some("tls") => fgex_proxy::proxy::Onward::Tls, None | Some("same") => fgex_proxy::proxy::Onward::Same, Some(other) => { eprintln!("[fatal] [main] FGEX_PROXY_TARGETS: unknown upstream '{other}'"); exit(2); } }; let public = parse_addr("FGEX_PROXY_TARGETS", address.trim()); // Said out loud, because everything else about an address is visible in the // rules and this is not: it lives in this process and nowhere else, so an // operator wondering why a port behaves the way it does has nothing to read. eprintln!( "[info] [main] {public} carries {} to {}", match edge { fgex_proxy::proxy::Edge::Tls => "TLS only", _ => "whatever arrives", }, match (target, upstream) { (Some(target), fgex_proxy::proxy::Onward::Plain) => { format!("{target} in the clear") } (Some(target), fgex_proxy::proxy::Onward::Tls) => format!("{target} on TLS"), (Some(target), _) => format!("{target} as it arrived"), (None, fgex_proxy::proxy::Onward::Plain) => "the service in the clear".into(), (None, fgex_proxy::proxy::Onward::Tls) => "the service on TLS".into(), (None, _) => "the service as it arrived".into(), } ); targets.publish(public, fgex_proxy::proxy::Published { target, edge, upstream }); } } let cfg = ProxyConfig { listen, upstream, spoof_source, connect_timeout: Duration::from_millis(connect_timeout), tls, // `None` simply means nothing is watching, which is the ordinary case. capture: capture.clone(), // Zero means no limit, which is what it was before there was one. The backend // always sends a value; the default here is for anyone running the engine by // hand, where a surprise limit would be worse than none. max_connections, over_limit_forwards, first_byte_timeout, self_mark: cfg_self_mark, targets: std::sync::Arc::clone(&targets), }; let chain = ChainHandle::new(FilterChain::new(filters, Duration::from_millis(deadline))); let chain_handle = chain.clone(); let proxy = match Proxy::bind(cfg, chain).await { Ok(p) => p, Err(e) => { eprintln!("[fatal] [main] cannot bind {listen}: {e}"); exit(1); } }; // Handshake, in the shape the other engines use (`QUEUE ` from cppregex): // one machine-readable line on stdout, so the caller can pass port 0 and learn // which port it actually got. Everything descriptive goes to stderr. let bound = proxy.local_addr().unwrap(); // Taken before the UDP relays are built: they share it, so one service reports one // pair of numbers whichever half of it the traffic arrived on. let counters = proxy.stats(); fgex_proxy::report::reply(format!("PORT {}", bound.port())); eprintln!( "[info] [main] listening on {bound} -> {upstream_raw} \ (spoof_source={spoof_source})" ); // The relays this service's addresses get, bound at startup and added to on demand. // Which kind is not a per-address choice: a service speaks QUIC or it speaks // datagrams, and the one it speaks is what every one of its addresses gets. let relays = if quic { let (cert, key) = match &material { Some(pair) => pair, None => { eprintln!( "[fatal] [main] a QUIC service needs a certificate: QUIC carries TLS 1.3 \ inside it, and terminating it is the only way a filter sees anything" ); exit(2); } }; let setup = match QuicSetup::build(cert, key) { Ok(setup) => Arc::new(setup), Err(e) => { eprintln!("[fatal] [main] {e}"); exit(2); } }; eprintln!( "[info] [main] QUIC terminated here; each client's own protocols are offered \ to the service" ); Relays::Quic(QuicManager::new( chain_handle.clone(), QuicConfig { setup, capture: capture.clone(), self_mark: cfg_self_mark, spoof_source, max_connections, over_limit_forwards, first_byte_timeout, connect_timeout: Duration::from_millis(connect_timeout), // The default for a relay that was added without saying; the backend // says, per address, on `FGEX_PROXY_UDP` and on `ADD_UDP`. upstream: fgex_proxy::proxy::Onward::Same, }, Arc::clone(&counters), )) } else { Relays::Datagram(fgex_proxy::udp::UdpManager::new( chain_handle.clone(), cfg_self_mark, spoof_source, max_connections, over_limit_forwards, Arc::clone(&counters), )) }; // UDP, when the backend asks for it: a comma-separated list of protected // addresses, one relay each. One socket per address rather than one for all of // them, because `SO_ORIGINAL_DST` — which is how the TCP side learns where a // connection was headed — is TCP and SCTP only. With the upstream fixed per socket // there is nothing to recover. if let Ok(spec) = std::env::var("FGEX_PROXY_UDP") { for target in spec.split(',').filter(|s| !s.trim().is_empty()) { // `
` or `
|`: what the service behind this one // relay speaks. Per relay because a relay is one protected address, and // two ports of one service can be reached differently. let (target, onward) = match target.trim().split_once('|') { Some((target, word)) => match fgex_proxy::proxy::Onward::from_word(word) { Some(onward) => (target, onward), None => { eprintln!("[fatal] [main] FGEX_PROXY_UDP: unknown upstream '{word}'"); exit(2); } }, None => (target.trim(), fgex_proxy::proxy::Onward::Same), }; let upstream = parse_addr("FGEX_PROXY_UDP", target.trim()); match relays.add_relay(upstream, onward).await { Ok(port) => { // With what it speaks onward, because that is half of what names a // relay: two addresses sending to one service port can want different // answers, and each gets a relay of its own. fgex_proxy::report::reply(format!("UDP {upstream}|{} {port}", onward.word())); } Err(e) => { eprintln!("[fatal] [main] cannot bind a UDP relay for {upstream}: {e}"); exit(1); } } } } // Rulesets and control commands (e.g. ADD_UDP) arrive on stdin from here on. // It runs alongside the datapath rather than gating it: traffic must flow // before, during and after a rule change or relay addition. let deadline_dur = Duration::from_millis(deadline); let control_chain = chain_handle.clone(); let control_relays = relays.clone(); tokio::spawn(async move { serve_stdin(control_chain, deadline_dur, control_relays, targets).await }); // Both counters live on the proxy, not on the chain: a chain is replaced wholesale // every time a ruleset is pushed, and counters that reset on a rule edit would make // the ratio jump for a reason nobody could see. tokio::spawn(async move { let mut tick = tokio::time::interval(STATS_INTERVAL); loop { // The first tick fires immediately, which is the point: zero out of zero is // a baseline, and "no answer yet" is not. tick.tick().await; // `live` and `over_limit` ride along with the two that were already here: // the backend parses this line as key/value pairs, so a new number costs no // protocol. `over_limit` is cumulative on purpose — a service that hit the // wall once an hour ago still says so, which is the whole point of a trace. // Dropped rather than waited for when the backend is behind: the next one // carries the same cumulative numbers two seconds later. fgex_proxy::report::event(format!( "STATS seen={} refused={} live={} over_limit={} no_first_byte={}", counters.accepted.load(std::sync::atomic::Ordering::Relaxed), counters.closed_by_filter.load(std::sync::atomic::Ordering::Relaxed), counters.live.load(std::sync::atomic::Ordering::Relaxed), counters.over_limit.load(std::sync::atomic::Ordering::Relaxed), counters.no_first_byte.load(std::sync::atomic::Ordering::Relaxed), )); } }); if let Err(e) = proxy.serve().await { eprintln!("[fatal] [main] accept loop died: {e}"); exit(1); } }