diff --git a/.gitignore b/.gitignore index c507849..82eaf26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target .idea +*.crt +*.key diff --git a/Cargo.lock b/Cargo.lock index 6884b95..995b79c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,7 @@ dependencies = [ "actix-codec", "actix-rt", "actix-service", + "actix-tls", "actix-utils", "ahash", "base64", @@ -143,6 +144,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-tls" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "log", + "pin-project-lite", + "tokio-rustls", + "tokio-util", + "webpki-roots", +] + [[package]] name = "actix-utils" version = "3.0.0" @@ -166,6 +185,7 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", + "actix-tls", "actix-utils", "actix-web-codegen", "ahash", @@ -1281,6 +1301,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1290,6 +1325,27 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1312,6 +1368,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.7.0" @@ -1440,6 +1506,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "strsim" version = "0.10.0" @@ -1477,6 +1549,7 @@ name = "tcp_relay_server" version = "0.1.0" dependencies = [ "actix", + "actix-tls", "actix-web", "actix-web-actors", "base", @@ -1484,6 +1557,8 @@ dependencies = [ "env_logger", "futures", "log", + "rustls", + "rustls-pemfile", "serde", "tokio", ] @@ -1612,6 +1687,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + [[package]] name = "tokio-tungstenite" version = "0.17.2" @@ -1717,6 +1803,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" @@ -1845,6 +1937,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +dependencies = [ + "webpki", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/tcp_relay_server/Cargo.toml b/tcp_relay_server/Cargo.toml index 4b5ebc8..f22f418 100644 --- a/tcp_relay_server/Cargo.toml +++ b/tcp_relay_server/Cargo.toml @@ -9,8 +9,11 @@ clap = { version = "3.2.18", features = ["derive", "env"] } log = "0.4.17" env_logger = "0.9.0" actix = "0.13.0" -actix-web = "4" +actix-web = { version = "4", features = ["rustls"] } actix-web-actors = "4.1.0" +actix-tls = "3.0.3" serde = { version = "1.0.144", features = ["derive"] } tokio = { version = "1", features = ["full"] } -futures = "0.3.24" \ No newline at end of file +futures = "0.3.24" +rustls = "0.20.6" +rustls-pemfile = "1.0.1" \ No newline at end of file diff --git a/tcp_relay_server/src/args.rs b/tcp_relay_server/src/args.rs index 422f453..b1096d3 100644 --- a/tcp_relay_server/src/args.rs +++ b/tcp_relay_server/src/args.rs @@ -2,8 +2,9 @@ use clap::Parser; /// TCP relay server #[derive(Parser, Debug, Clone)] -#[clap(author, version, about, long_about = None)] -pub struct Args { +#[clap(author, version, about, + long_about = "TCP-over-HTTP server. This program might be configured behind a reverse-proxy.")] +pub struct ProgramArgs { /// Access tokens #[clap(short, long)] pub tokens: Vec, @@ -28,4 +29,12 @@ pub struct Args { /// on the same machine #[clap(short, long, default_value_t = 0)] pub increment_ports: u16, + + /// TLS certificate. Specify also private key to use HTTPS/TLS instead of HTTP + #[clap(long)] + pub tls_cert: Option, + + /// TLS private key. Specify also certificate to use HTTPS/TLS instead of HTTP + #[clap(long)] + pub tls_key: Option, } \ No newline at end of file diff --git a/tcp_relay_server/src/main.rs b/tcp_relay_server/src/main.rs index bb008c2..ec0cf9b 100644 --- a/tcp_relay_server/src/main.rs +++ b/tcp_relay_server/src/main.rs @@ -1,18 +1,22 @@ +use std::fs::File; +use std::io::BufReader; use std::sync::Arc; use actix_web::{App, HttpRequest, HttpResponse, HttpServer, middleware, Responder, web}; use actix_web::web::Data; use clap::Parser; +use rustls::{Certificate, PrivateKey, ServerConfig}; +use rustls_pemfile::{certs, Item, read_one}; use base::RelayedPort; -use tcp_relay_server::args::Args; +use tcp_relay_server::args::ProgramArgs; use tcp_relay_server::relay_ws::relay_ws; pub async fn hello_route() -> &'static str { "Hello world!" } -pub async fn config_route(req: HttpRequest, data: Data>) -> impl Responder { +pub async fn config_route(req: HttpRequest, data: Data>) -> impl Responder { let token = req.headers().get("Authorization") .map(|t| t.to_str().unwrap_or_default()) .unwrap_or_default() @@ -35,13 +39,49 @@ pub async fn config_route(req: HttpRequest, data: Data>) -> impl Respo async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - let mut args: Args = Args::parse(); + let mut args: ProgramArgs = ProgramArgs::parse(); if args.ports.is_empty() { log::error!("No port to forward!"); std::process::exit(2); } + // Load TLS configuration, if any + let tls_config = if let (Some(cert), Some(key)) = (&args.tls_cert, &args.tls_key) { + + // Load TLS certificate & private key + let cert_file = &mut BufReader::new(File::open(cert).unwrap()); + let key_file = &mut BufReader::new(File::open(key).unwrap()); + + // Get certificates chain + let cert_chain = certs(cert_file).unwrap() + .into_iter() + .map(Certificate) + .collect(); + + // Get private key + let key = match read_one(key_file).expect("Failed to read private key!") { + None => { + log::error!("Failed to extract private key!"); + panic!(); + } + Some(Item::PKCS8Key(key)) => key, + Some(Item::RSAKey(key)) => key, + _ => { + log::error!("Unsupported private key type!"); + panic!(); + } + }; + + let config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(key)) + .expect("Failed to load TLS certificate!"); + + Some(config) + } else { None }; + // Read tokens from file, if any if let Some(file) = &args.tokens_file { std::fs::read_to_string(file) @@ -60,15 +100,19 @@ async fn main() -> std::io::Result<()> { let args = Arc::new(args); let args_clone = args.clone(); - HttpServer::new(move || { + let server = HttpServer::new(move || { App::new() .wrap(middleware::Logger::default()) .app_data(Data::new(args_clone.clone())) .route("/", web::get().to(hello_route)) .route("/config", web::get().to(config_route)) .route("/ws", web::get().to(relay_ws)) - }) - .bind(&args.listen_address)? - .run() + }); + + if let Some(tls_conf) = tls_config { + server.bind_rustls(&args.listen_address, tls_conf)? + } else { + server.bind(&args.listen_address)? + }.run() .await } \ No newline at end of file diff --git a/tcp_relay_server/src/relay_ws.rs b/tcp_relay_server/src/relay_ws.rs index 1fcfb33..45bfdbd 100644 --- a/tcp_relay_server/src/relay_ws.rs +++ b/tcp_relay_server/src/relay_ws.rs @@ -9,7 +9,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; use tokio::net::TcpStream; -use crate::args::Args; +use crate::args::ProgramArgs; /// How often heartbeat pings are sent const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); @@ -153,7 +153,7 @@ pub struct WebSocketQuery { pub async fn relay_ws(req: HttpRequest, stream: web::Payload, query: web::Query, - conf: web::Data>) -> Result { + conf: web::Data>) -> Result { if !conf.tokens.contains(&query.token) { log::error!("Rejected WS request from {:?} due to invalid token!", req.peer_addr()); return Ok(HttpResponse::Unauthorized().json("Invalid / missing token!"));