feat: project skeleton, CLI surface, generate command, secret redaction, and CI matrix
- Create Rust binary crate with clap CLI (listen, connect, generate, version) - Implement generate command: CA/server/client certs, auth token, config.json - Strict secret redaction: Redacted type, PEM/token/config redaction utilities - Configurable addresses and ports with validation (fails before sockets open) - GitHub Actions CI matrix: Windows/Linux/macOS with fmt, clippy, test, build, release-build, and smoke check jobs - 39 unit tests covering CLI parsing, redaction, config, and credential generation - Cross-platform safe paths and clean-room implementation
This commit is contained in:
+216
@@ -0,0 +1,216 @@
|
||||
mod cli;
|
||||
mod config;
|
||||
mod generate;
|
||||
mod redact;
|
||||
|
||||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::cli::Cli;
|
||||
|
||||
fn main() {
|
||||
// Initialize tracing/logger — secrets are never printed by design
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.without_time()
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let exit_code = match cli.command {
|
||||
crate::cli::Commands::Listen {
|
||||
listen,
|
||||
cert,
|
||||
key,
|
||||
ca_cert,
|
||||
auth_token,
|
||||
} => run_listen(listen, cert, key, ca_cert, auth_token),
|
||||
crate::cli::Commands::Connect {
|
||||
target,
|
||||
socks,
|
||||
cert,
|
||||
key,
|
||||
ca_cert,
|
||||
auth_token,
|
||||
} => run_connect(target, socks, cert, key, ca_cert, auth_token),
|
||||
crate::cli::Commands::Generate {
|
||||
out,
|
||||
ca_name,
|
||||
server_name,
|
||||
client_name,
|
||||
} => run_generate(&out, &ca_name, &server_name, &client_name),
|
||||
crate::cli::Commands::Version => run_version(),
|
||||
};
|
||||
|
||||
process::exit(exit_code);
|
||||
}
|
||||
|
||||
fn run_listen(
|
||||
listen: String,
|
||||
cert: String,
|
||||
key: String,
|
||||
ca_cert: String,
|
||||
auth_token: String,
|
||||
) -> i32 {
|
||||
// Validate all inputs before opening any sockets
|
||||
if cert.is_empty() {
|
||||
eprintln!("Error: --cert is required for listen command");
|
||||
return 1;
|
||||
}
|
||||
if key.is_empty() {
|
||||
eprintln!("Error: --key is required for listen command");
|
||||
return 1;
|
||||
}
|
||||
if ca_cert.is_empty() {
|
||||
eprintln!("Error: --ca-cert is required for listen command");
|
||||
return 1;
|
||||
}
|
||||
if auth_token.is_empty() {
|
||||
eprintln!("Error: --auth-token is required for listen command");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let (host, port) = match cli::parse_host_port(&listen) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Verify files exist before opening sockets
|
||||
if !std::path::Path::new(&cert).exists() {
|
||||
eprintln!("Error: server certificate not found: {}", cert);
|
||||
return 1;
|
||||
}
|
||||
if !std::path::Path::new(&key).exists() {
|
||||
eprintln!("Error: server key not found: {}", key);
|
||||
return 1;
|
||||
}
|
||||
if !std::path::Path::new(&ca_cert).exists() {
|
||||
eprintln!("Error: CA certificate not found: {}", ca_cert);
|
||||
return 1;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Starting HTTPS tunnel listener on {}:{} (HTTPS default transport)",
|
||||
host,
|
||||
port
|
||||
);
|
||||
tracing::info!("Using server certificate: {}", redact::Redacted::new(&cert));
|
||||
tracing::info!("Using server key: {}", redact::Redacted::new(&key));
|
||||
tracing::info!("Using CA certificate: {}", redact::Redacted::new(&ca_cert));
|
||||
tracing::info!(
|
||||
"Auth token configured: {}",
|
||||
redact::Redacted::new(&auth_token)
|
||||
);
|
||||
|
||||
// TODO: Implement HTTPS mTLS listener
|
||||
tracing::info!("Listener implementation pending (Milestone 2)");
|
||||
0
|
||||
}
|
||||
|
||||
fn run_connect(
|
||||
target: String,
|
||||
socks: String,
|
||||
cert: String,
|
||||
key: String,
|
||||
ca_cert: String,
|
||||
auth_token: String,
|
||||
) -> i32 {
|
||||
// Validate all inputs before opening any sockets
|
||||
if cert.is_empty() {
|
||||
eprintln!("Error: --cert is required for connect command");
|
||||
return 1;
|
||||
}
|
||||
if key.is_empty() {
|
||||
eprintln!("Error: --key is required for connect command");
|
||||
return 1;
|
||||
}
|
||||
if ca_cert.is_empty() {
|
||||
eprintln!("Error: --ca-cert is required for connect command");
|
||||
return 1;
|
||||
}
|
||||
if auth_token.is_empty() {
|
||||
eprintln!("Error: --auth-token is required for connect command");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let (_target_host, target_port) = match cli::parse_host_port(&target) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
let (socks_host, socks_port) = match cli::parse_host_port(&socks) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Verify files exist before opening any sockets
|
||||
if !std::path::Path::new(&cert).exists() {
|
||||
eprintln!("Error: client certificate not found: {}", cert);
|
||||
return 1;
|
||||
}
|
||||
if !std::path::Path::new(&key).exists() {
|
||||
eprintln!("Error: client key not found: {}", key);
|
||||
return 1;
|
||||
}
|
||||
if !std::path::Path::new(&ca_cert).exists() {
|
||||
eprintln!("Error: CA certificate not found: {}", ca_cert);
|
||||
return 1;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Connecting to HTTPS tunnel at {}:{} (HTTPS default transport)",
|
||||
_target_host,
|
||||
target_port
|
||||
);
|
||||
tracing::info!("SOCKS5 proxy will listen on {}:{} ", socks_host, socks_port);
|
||||
tracing::info!("Using client certificate: {}", redact::Redacted::new(&cert));
|
||||
tracing::info!("Using client key: {}", redact::Redacted::new(&key));
|
||||
tracing::info!("Using CA certificate: {}", redact::Redacted::new(&ca_cert));
|
||||
tracing::info!(
|
||||
"Auth token configured: {}",
|
||||
redact::Redacted::new(&auth_token)
|
||||
);
|
||||
|
||||
// TODO: Implement HTTPS mTLS connector and SOCKS5 proxy
|
||||
tracing::info!("Connector and SOCKS5 implementation pending (Milestone 2-3)");
|
||||
0
|
||||
}
|
||||
|
||||
fn run_generate(out: &str, ca_name: &str, server_name: &str, client_name: &str) -> i32 {
|
||||
let out_dir = std::path::Path::new(out);
|
||||
|
||||
match generate::generate(out_dir, ca_name, server_name, client_name) {
|
||||
Ok(()) => {
|
||||
println!("Credential material generated in {}", out_dir.display());
|
||||
println!(
|
||||
"Files created: ca.pem, ca.key, server.crt, server.key, client.crt, client.key, token.txt, config.json"
|
||||
);
|
||||
0
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error generating credentials: {}", e);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_version() -> i32 {
|
||||
println!("rustunnel {}", env!("CARGO_PKG_VERSION"));
|
||||
println!("Edition: {}", env!("CARGO_PKG_VERSION"));
|
||||
println!("Platform: {}", std::env::consts::OS);
|
||||
println!("Arch: {}", std::env::consts::ARCH);
|
||||
0
|
||||
}
|
||||
Reference in New Issue
Block a user