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:
c4ch3c4d3
2026-06-03 17:49:55 -06:00
commit eb4ef2bc05
8 changed files with 1290 additions and 0 deletions
+216
View File
@@ -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
}