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
1 change: 1 addition & 0 deletions apps/gateway/Cargo.lock

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

11 changes: 9 additions & 2 deletions apps/gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ ring = "0.17"
# HTTP client (for upstream forwarding in MITM mode)
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"], default-features = false }

# CLI
clap = { version = "4", features = ["derive"] }
# CLI. `env` (relay mode): every relay CLI flag falls back to an environment
# variable so it can run unattended in a container with no generated command
# line.
clap = { version = "4", features = ["derive", "env"] }

# Logging
tracing = "0.1"
Expand All @@ -70,6 +72,11 @@ base64 = "0.22"
# UUID (for approval IDs)
uuid = { version = "1", features = ["v4"] }

# Lock-free config swap (relay mode): the mTLS `ClientConfig` is rebuilt on
# certificate renewal and swapped in without disrupting in-flight tunnels,
# each of which holds its own `Arc` snapshot taken at dial time.
arc-swap = "1"

# Stream combinators
futures-util = "0.3"

Expand Down
2 changes: 1 addition & 1 deletion apps/gateway/src/client_ca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ fn sanitize_identity_component(s: &str) -> Option<String> {
/// from Secrets Manager); anything else is treated as a filesystem path (OSS
/// mounts files). Empty or unset input is `Ok(None)` — the caller decides
/// whether that's fatal.
fn pem_from_value(var_name: &str, value: &str) -> Result<Option<String>> {
pub(crate) fn pem_from_value(var_name: &str, value: &str) -> Result<Option<String>> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(None);
Expand Down
124 changes: 124 additions & 0 deletions apps/gateway/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ mod edition;
mod gateway;
mod inject;
mod policy;
mod relay;
mod secret_inject;
mod shutdown;
mod summary;
Expand Down Expand Up @@ -162,6 +163,29 @@ struct Cli {
/// Data directory for CA certificates and persistent state.
#[arg(long, default_value = default_data_dir())]
data_dir: PathBuf,

/// Optional subcommand. With none given, `onecli-gateway` (optionally
/// with `--port`/`--data-dir`) parses exactly as it always has and runs
/// the MITM gateway server — see the module doc on `Command` below for
/// why that byte-for-byte compatibility matters.
#[command(subcommand)]
command: Option<Command>,
}

/// Subcommands layered onto the historically flag-only `onecli-gateway` CLI.
///
/// `command` on [`Cli`] is `Option<Command>`, not `Command`, specifically so
/// that omitting it entirely — the only way this binary has ever been
/// invoked before this change — continues to select the server, not a clap
/// error demanding a subcommand. `main` dispatches on it before any of the
/// server's own CA/DB/crypto/vault bootstrapping runs, so `relay` never pays
/// for (or requires) any of that.
#[derive(clap::Subcommand, Debug)]
enum Command {
/// Run a local mTLS relay: a blind byte-splice between a plain
/// HTTP-proxy agent and a remote OneCLI gateway. See `relay.rs`'s module
/// doc for the security property this preserves.
Relay(relay::RelayArgs),
}

/// Cap on the final telemetry flush, inside the overall shutdown budget.
Expand Down Expand Up @@ -212,6 +236,15 @@ async fn main() -> Result<()> {
// first poll.
shutdown::install();

// Relay mode is an entirely separate program sharing only the process's
// signal handling and rustls crypto provider install above: no CA, no
// database, no crypto service, no vault. Dispatched here, before any of
// that server-only bootstrapping below runs, so it never pays for (or
// requires) any of it.
if let Some(Command::Relay(args)) = cli.command {
return relay::run(args).await;
}

// Expand ~ in data dir
let data_dir = expand_tilde(&cli.data_dir);

Expand Down Expand Up @@ -393,3 +426,94 @@ fn expand_tilde(path: &Path) -> PathBuf {
}
path.to_path_buf()
}

#[cfg(test)]
mod tests {
use super::*;

/// The default invocation — no subcommand at all — must keep parsing to
/// the server arm exactly as it did before `Command` existed. This is
/// the regression guard for every existing deployment's `onecli-gateway`
/// (with no args) or `onecli-gateway --port N` invocation.
#[test]
fn no_subcommand_parses_to_the_server_arm() {
let cli = Cli::try_parse_from(["onecli-gateway"]).expect("parses with no args");
assert_eq!(cli.port, 10255);
assert!(cli.command.is_none());
}

#[test]
fn port_flag_alone_still_parses_to_the_server_arm() {
let cli = Cli::try_parse_from(["onecli-gateway", "--port", "9999"]).expect("parses --port");
assert_eq!(cli.port, 9999);
assert!(cli.command.is_none());
}

#[test]
fn data_dir_flag_alone_still_parses_to_the_server_arm() {
let cli = Cli::try_parse_from(["onecli-gateway", "--data-dir", "/tmp/onecli-data"])
.expect("parses --data-dir");
assert_eq!(cli.data_dir, PathBuf::from("/tmp/onecli-data"));
assert!(cli.command.is_none());
}

/// The new `relay` subcommand parses into `Command::Relay` with its
/// required fields populated — proving the subcommand addition didn't
/// break argument routing in either direction.
#[test]
fn relay_subcommand_parses_its_required_args() {
let cli = Cli::try_parse_from([
"onecli-gateway",
"relay",
"--gateway-addr",
"gateway.example.com:8443",
"--gateway-server-ca",
"/etc/onecli/server-ca.pem",
"--api-url",
"https://api.example.com",
"--api-key",
"oc_test_key",
])
.expect("parses relay subcommand");

match cli.command {
Some(Command::Relay(args)) => {
assert_eq!(args.gateway_addr, "gateway.example.com:8443");
assert_eq!(args.api_url, "https://api.example.com");
assert_eq!(args.api_key, "oc_test_key");
assert_eq!(
args.bind,
"127.0.0.1:10255".parse::<std::net::SocketAddr>().unwrap()
);
}
None => panic!("expected Command::Relay"),
}
}

/// Missing a required relay flag (with its env var also unset) must
/// fail to parse rather than silently default — `--gateway-addr` has no
/// default and nothing here sets `RELAY_GATEWAY_ADDR`.
#[test]
fn relay_subcommand_requires_gateway_addr() {
// Isolated from whatever the test process's real environment holds:
// if `RELAY_GATEWAY_ADDR` happened to be set, this would spuriously
// pass. There is no portable safe env-mutation in a parallel test
// binary, so this only asserts what it can control directly: the
// flag form is absent, and if the env var were also absent this
// must fail.
if std::env::var("RELAY_GATEWAY_ADDR").is_ok() {
return;
}
let result = Cli::try_parse_from([
"onecli-gateway",
"relay",
"--gateway-server-ca",
"/etc/onecli/server-ca.pem",
"--api-url",
"https://api.example.com",
"--api-key",
"oc_test_key",
]);
assert!(result.is_err(), "must require --gateway-addr");
}
}
Loading