--- /dev/null
+image: archlinux
+packages:
+ - make
+ - rsync
+ - coreutils
+ - lld
+ - rustup
+ - aarch64-linux-gnu-gcc
+ - tar
+ - gzip
+sources:
+ - git@git.sr.ht:~rbdr/olden-mail
+secrets:
+ - 89d3b676-25d6-4942-8231-38b73aa62bf6
+ - 0b0d3e5e-fbdc-41d0-97ed-ee654fe797ff
+tasks:
+ - set_rust: |
+ cd olden-mail
+ make set_rust
+ - install_coverage_tool: |
+ cargo install cargo-tarpaulin
+ - install_builders: |
+ cargo install cargo-generate-rpm
+ cargo install cargo-deb
+ - package: |
+ cd page
+ make ci
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+dependencies = [
+ "anstyle",
+ "once_cell",
+ "windows-sys",
+]
+
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+[[package]]
+name = "ctrlc"
+version = "3.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
+dependencies = [
+ "nix",
+ "windows-sys",
+]
+
+[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
+]
+
[[package]]
name = "errno"
version = "0.3.10"
"wasi",
]
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
[[package]]
name = "native-tls"
version = "0.2.12"
"tempfile",
]
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
[[package]]
name = "olden-mail"
-version = "0.1.0"
+version = "1.0.0"
dependencies = [
+ "ctrlc",
+ "env_logger",
+ "log",
"native-tls",
+ "thiserror",
]
[[package]]
"proc-macro2",
]
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
[[package]]
name = "rustix"
version = "0.38.44"
"windows-sys",
]
+[[package]]
+name = "thiserror"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
[[package]]
name = "vcpkg"
version = "0.2.15"
[package]
name = "olden-mail"
-version = "0.1.0"
+version = "1.0.0"
edition = "2021"
+license = "AGPL-3.0-or-later"
+description = "IMAP & SMTP proxy to connect vintage clients to modern e-mail."
+homepage = "https://r.bdr.sh/olden-mail.html"
+authors = ["Rubén Beltrán del Río <olden.mail@r.bdr.sh>"]
[dependencies]
+ctrlc = "3.4.5"
+env_logger = "0.11.6"
+log = "0.4.25"
native-tls = "0.2.12"
+thiserror = "2.0.11"
+
+[profile.release]
+strip = true
+lto = true
+panic = "abort"
+
+[package.metadata.generate-rpm]
+assets = [
+ { source = "target/release/olden-mail", dest = "/usr/bin/olden-mail", mode = "755" },
+ { source = "man/olden-mail.1", dest = "/usr/share/man/man1/olden-mail.1", mode = "644" },
+]
+
+[package.metadata.deb]
+assets = [
+ ["target/release/olden-mail", "/usr/bin/olden-mail", "755" ],
+ ["man/olden-mail.1", "/usr/share/man/man1/olden-mail.1", "644" ],
+]
+
+[lints.clippy]
+pedantic = "warn"
--- /dev/null
+# olden-mail
+
+`olden-mail` is an IMAP & SMTP proxy that allows you to connect to SSL
+enabled mail servers using insecure plaintext connections.
+
+This is of course very insecure, but it's intended to allow vintage
+computers that don't have SSL capability or ther right ciphers to still
+be used for e-mail.
+
+## Installation
+
+### Pre-built binaries and packages
+
+Binaries are available for macos and linux, for both aarch64 and x86_64
+from https://build.r.bdr.sh/olden-mail, including `.deb` and `.rpm`
+packages.
+
+### Homebrew
+
+You can also install this on macos via homebrew.
+
+```
+% brew tap rbdr/apps git@git.sr.ht:~rbdr/homebrew-apps
+% brew install rbdr/apps/olden-mail
+```
+
+### From source
+
+You can run `make` for a debug build, or `make -e profile=release` for a
+release build.
+
+## Usage
+
+The proxy requires you to set environment variables, but otherwise takes
+no options.
+
+* `LOCAL_IMAP_PORT` u16, the port in which the server will listen for
+IMAP clients. Defaults to 143.
+* `LOCAL_IMAP_BIND_ADDRESS` String, the address on which to listen for
+IMAP clients. Defaults to 0.0.0.0.
+* `REMOTE_IMAP_PORT` u16, the port to which the server will forward the
+IMAP messages. Defaults to 993.
+* `REMOTE_IMAP_HOST` String, the host to which the server will forward the
+IMAP messages. Required.
+
+* `LOCAL_SMTP_PORT` u16, the port in which the server will listen for
+SMTP clients. Defaults to 25.
+* `LOCAL_SMTP_BIND_ADDRESS` String, the address on which to listen for
+SMTP clients. Defaults to 0.0.0.0.
+* `REMOTE_SMTP_PORT` u16, the port to which the server will forward the
+SMTP messages. Defaults to 465.
+* `REMOTE_SMTP_HOST` String, the host to which the server will forward the
+SMTP messages. Required.
+
+This means the minimum invocation is this (Shown here with inline
+environment variables)
+
+```
+% REMOTE_IMAP_DOMAIN=imap.coolmailsite.example REMOTE_SMTP_DOMAIN=smtp.coolmailsite.example olden-mail
+```
+
+## Debugging
+
+You can control how much it prints by setting `RUST_LOG`. Setting it to
+`debug` will output the whole protocol stream. The default level is
+`info`.
--- /dev/null
+.TH OLDEN-MAIL 1 "2025-01-25" "1.0.0" "Olden Mail Manual"
+.SH NAME
+olden-mail \- IMAP & SMTP proxy to connect vintage clients to modern e-mail.
+.SH SYNOPSIS
+.B olden-mail
+.SH DESCRIPTION
+.PP
+`olden-mail` is an IMAP & SMTP proxy that allows you to connect to SSL
+enabled mail servers using insecure plaintext connections in computers that
+don't support SSL or have older ciphers.
+.SH ENVIRONMENT VARIABLES
+You can configure the proxy through environment variables.
+.TP
+.B LOCAL_IMAP_PORT
+The port on which to listen for IMAP clients. Defaults to 143.
+.TP
+.B LOCAL_IMAP_BIND_ADDRESS
+The address on which to listen for IMAP clients. Defaults to 0.0.0.0.
+.TP
+.B REMOTE_IMAP_PORT
+The TLS enabled remote port to which we will proxy the connection. Defaults to
+993.
+.TP
+.B REMOTE_IMAP_HOST
+The remote host to which we will proxy the connection. Required.
+.TP
+.B LOCAL_SMTP_PORT
+The port on which to listen for SMTP clients. Defaults to 25.
+.TP
+.B LOCAL_SMTP_BIND_ADDRESS
+The address on which to listen for SMTP clients. Defaults to 0.0.0.0.
+.TP
+.B REMOTE_SMTP_PORT
+The TLS enabled remote port to which we will proxy the connection. Defaults to
+465.
+.TP
+.B REMOTE_SMTP_HOST
+The remote host to which we will proxy the connection. Required.
+.SH VERSION
+.BR 1.0.0
+.SH HOMEPAGE
+.I https://r.bdr.sh/olden-mail.html
+.SH AUTHORS
+\ Rubén Beltrán del Río <\fIolden.mail@r.bdr.sh\fP>
+//! # Configuration
+//!
+//! This module defines the configuration used to initialize both proxies.
+
+use log::error;
use std::env;
+use std::fmt::Debug;
use std::sync::Arc;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ConfigurationError {
+ #[error("Environment variable {0} not set.")]
+ MissingEnvVar(String),
+ #[error("Failed to parse {0}.")]
+ ParseError(String),
+}
+/// Configuration for any proxy
+#[derive(Debug)]
pub struct ProxyConfiguration {
pub local_port: u16,
- pub remote_domain: String,
+ pub bind_address: String,
+ pub remote_host: String,
pub remote_port: u16,
pub protocol: &'static str,
}
+/// Aggregated configuration for both proxies, already in a reference counter.
pub struct Configuration {
pub imap_configuration: Arc<ProxyConfiguration>,
pub smtp_configuration: Arc<ProxyConfiguration>,
}
impl Configuration {
- pub fn new() -> Self {
- Configuration {
+ /// Creates a new configuration object by reading the environment
+ /// variables. Exits if the right ones aren't found.
+ pub fn new() -> Result<Self, ConfigurationError> {
+ Ok(Configuration {
imap_configuration: Arc::new(ProxyConfiguration {
- local_port: env::var("LOCAL_IMAP_PORT")
- .expect("LOCAL_IMAP_PORT not set")
- .parse()
- .expect("Invalid LOCAL_IMAP_PORT"),
- remote_domain: env::var("REMOTE_IMAP_DOMAIN").expect("REMOTE_IMAP_DOMAIN not set"),
- remote_port: env::var("REMOTE_IMAP_PORT")
- .expect("REMOTE_IMAP_PORT not set")
- .parse()
- .expect("Invalid REMOTE_IMAP_PORT"),
+ local_port: get_env_number("LOCAL_IMAP_PORT", 143)?,
+ bind_address: get_env_var("LOCAL_IMAP_BIND_ADDRESS", Some("0.0.0.0".to_string()))?,
+ remote_host: get_env_var("REMOTE_IMAP_DOMAIN", None)?,
+ remote_port: get_env_number("REMOTE_IMAP_PORT", 993)?,
protocol: "IMAP",
}),
smtp_configuration: Arc::new(ProxyConfiguration {
- local_port: env::var("LOCAL_SMTP_PORT")
- .expect("LOCAL_SMTP_PORT not set")
- .parse()
- .expect("Invalid LOCAL_SMTP_PORT"),
- remote_domain: env::var("REMOTE_SMTP_DOMAIN").expect("REMOTE_SMTP_DOMAIN not set"),
- remote_port: env::var("REMOTE_SMTP_PORT")
- .expect("REMOTE_SMTP_PORT not set")
- .parse()
- .expect("Invalid REMOTE_SMTP_PORT"),
+ local_port: get_env_number("LOCAL_SMTP_PORT", 25)?,
+ bind_address: get_env_var("LOCAL_SMTP_BIND_ADDRESS", Some("0.0.0.0".to_string()))?,
+ remote_host: get_env_var("REMOTE_SMTP_DOMAIN", None)?,
+ remote_port: get_env_number("REMOTE_SMTP_PORT", 993)?,
protocol: "SMTP",
}),
- }
+ })
+ }
+}
+
+/// Get an environment variable or return an error.
+fn get_env_var(name: &str, default: Option<String>) -> Result<String, ConfigurationError> {
+ match env::var(name) {
+ Ok(value) => Ok(value),
+ Err(_) => match default {
+ Some(default_value) => Ok(default_value),
+ None => Err(ConfigurationError::MissingEnvVar(name.to_string())),
+ },
+ }
+}
+
+/// Get an environment variable and parse it as a number. Return a default
+/// if not set.
+fn get_env_number(name: &str, default: u16) -> Result<u16, ConfigurationError> {
+ match env::var(name) {
+ Ok(value) => value
+ .parse()
+ .map_err(|_| ConfigurationError::ParseError(name.to_string())),
+ Err(_) => Ok(default),
}
}
+//! # olden-mail
+//!
+//! `olden-mail` is an IMAP & SMTP proxy that allows you to connect to SSL
+//! enabled mail servers using insecure plaintext connections.
+//!
+//! This is of course very insecure, but it's intended to allow vintage
+//! computers that don't have SSL capability or ther right ciphers to still
+//! be used for e-mail.
+//!
+//! ## Installation
+//!
+//! ### Pre-built binaries and packages
+//!
+//! Binaries are available for macos and linux, for both aarch64 and x86_64
+//! from https://build.r.bdr.sh/olden-mail, including `.deb` and `.rpm`
+//! packages.
+//!
+//! ### Homebrew
+//!
+//! You can also install this on macos via homebrew.
+//!
+//! ```
+//! % brew tap rbdr/apps git@git.sr.ht:~rbdr/homebrew-apps
+//! % brew install rbdr/apps/olden-mail
+//! ```
+//!
+//! ### From source
+//!
+//! You can run `make` for a debug build, or `make -e profile=release` for a
+//! release build.
+//!
+//! ## Usage
+//!
+//! The proxy requires you to set environment variables, but otherwise takes
+//! no options.
+//!
+//! * `LOCAL_IMAP_PORT` u16, the port in which the server will listen for
+//! IMAP clients. Defaults to 143.
+//! * `LOCAL_IMAP_BIND_ADDRESS` String, the address on which to listen for
+//! IMAP clients. Defaults to 0.0.0.0.
+//! * `REMOTE_IMAP_PORT` u16, the port to which the server will forward the
+//! IMAP messages. Defaults to 993.
+//! * `REMOTE_IMAP_HOST` String, the host to which the server will forward the
+//! IMAP messages. Required.
+//!
+//! * `LOCAL_SMTP_PORT` u16, the port in which the server will listen for
+//! SMTP clients. Defaults to 25.
+//! * `LOCAL_SMTP_BIND_ADDRESS` String, the address on which to listen for
+//! SMTP clients. Defaults to 0.0.0.0.
+//! * `REMOTE_SMTP_PORT` u16, the port to which the server will forward the
+//! SMTP messages. Defaults to 465.
+//! * `REMOTE_SMTP_HOST` String, the host to which the server will forward the
+//! SMTP messages. Required.
+//!
+//! This means the minimum invocation is this (Shown here with inline
+//! environment variables)
+//!
+//! ```
+//! % REMOTE_IMAP_DOMAIN=imap.coolmailsite.example REMOTE_SMTP_DOMAIN=smtp.coolmailsite.example olden-mail
+//! ```
+//!
+//! ## Debugging
+//!
+//! You can control how much it prints by setting `RUST_LOG`. Setting it to
+//! `debug` will output the whole protocol stream. The default level is
+//! `info`.
+
+use env_logger::{Builder, Env};
+use log::{error, info};
+use std::process::exit;
+use std::sync::mpsc;
+
mod configuration;
mod proxy;
use configuration::Configuration;
-use proxy::create_proxy;
+use proxy::ProxyServer;
fn main() {
- let configuration = Configuration::new();
+ Builder::from_env(Env::default().default_filter_or("info"))
+ .format_timestamp_millis()
+ .init();
+
+ let configuration = Configuration::new().unwrap_or_else(|error| {
+ error!("{error}");
+ exit(1);
+ });
+
+ let (tx, rx) = mpsc::channel();
+
+ let mut imap_proxy = ProxyServer::new(configuration.imap_configuration);
+ let mut smtp_proxy = ProxyServer::new(configuration.smtp_configuration);
- let imap_proxy = create_proxy(configuration.imap_configuration);
- let smtp_proxy = create_proxy(configuration.smtp_configuration);
+ ctrlc::set_handler(move || {
+ info!("Shutting down...");
+ imap_proxy.shutdown();
+ smtp_proxy.shutdown();
+ let _ = tx.send(());
+ })
+ .unwrap_or_else(|e| {
+ error!("Error setting Ctrl-C handler: {e}");
+ std::process::exit(1);
+ });
- imap_proxy.join().unwrap();
- smtp_proxy.join().unwrap();
+ // Block until we get a shutdown
+ rx.recv().unwrap_or(());
}
+//! # Proxy Module
+//!
+//! This module has the actual proxy functionality, exposed through
+//! `ProxyServer`. The proxy consists of a local unencrypted TCP stream
+//! and a remote TLS stream. Messages are passed between them via two
+//! threads.
+//!
+//! Each new client connection spawns two threads:
+//! - **Client to Server Thread**: Forwards data from client -> TLS server
+//! - **Serveer to Client Thread**: Forwards data from TLS server -> client
+//!
+//! Finally, the ProxyServer may be shutdown by calling `.shutdown()`,
+//! this will stop new connections and wait for it to finish.
+//!
+//! # Example
+//!
+//! ```
+//! use std::sync::Arc;
+//! use crate::configuration::ProxyConfiguration;
+//! use crate::proxy::ProxyServer;
+//!
+//! let config = Arc::new(ProxyConfiguration {
+//! protocol: "IMAP".to_string(),
+//! local_port: 143,
+//! remote_domain: "imap.example.com".to_string(),
+//! remote_port: 993,
+//! });
+//!
+//! let mut server = ProxyServer::new(config);
+//! // The server runs in a background thread. To shut down gracefully:
+//! server.shutdown();
+//! ```
+use log::{debug, error, info};
use native_tls::TlsConnector;
-use std::io::{Read, Write};
+use std::io::{ErrorKind, Read, Write};
use std::net::{TcpListener, TcpStream};
-use std::sync::Arc;
-use std::thread::{spawn, JoinHandle};
+use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc, Mutex,
+};
+use std::thread::{sleep, spawn, JoinHandle};
+use std::time::Duration;
use crate::configuration::ProxyConfiguration;
-pub fn create_proxy(configuration: Arc<ProxyConfiguration>) -> JoinHandle<()> {
- let cloned_configuration = Arc::clone(&configuration);
- spawn(move || {
- run_proxy(cloned_configuration);
- })
+/// A proxy server that listens for plaintext connections and forwards them
+/// via TLS.
+///
+/// Creating a new `ProxyServer` spawns a dedicated thread that:
+/// - Binds to a local port (non-blocking mode).
+/// - Spawns additional threads for each incoming client connection.
+/// - Manages connection-lifetime until it receives a shutdown signal.
+pub struct ProxyServer {
+ running: Arc<AtomicBool>,
+ thread_handle: Option<JoinHandle<()>>,
}
-fn run_proxy(configuration: Arc<ProxyConfiguration>) {
- let listener = TcpListener::bind(format!("0.0.0.0:{}", configuration.local_port)).unwrap();
+impl ProxyServer {
+ /// Creates a new `ProxyServer` for the given `ProxyConfiguration` and
+ /// immediately starts it.
+ ///
+ /// # Arguments
+ ///
+ /// * `configuration` - Shared (Arc) `ProxyConfiguration`
+ ///
+ /// # Returns
+ ///
+ /// A `ProxyServer` instance that will keep running until its `.shutdown()`
+ /// method is called, or an error occurs.
+ pub fn new(configuration: Arc<ProxyConfiguration>) -> Self {
+ let running = Arc::new(AtomicBool::new(true));
+ let running_clone = Arc::clone(&running);
- println!("Proxy listening on port {}", configuration.local_port);
+ let thread_handle = spawn(move || {
+ run_proxy(configuration, running_clone);
+ });
- for stream in listener.incoming() {
- match stream {
- Ok(stream) => {
- let cloned_configuration = Arc::clone(&configuration);
- spawn(move || {
- handle_client(stream, cloned_configuration);
+ ProxyServer {
+ running,
+ thread_handle: Some(thread_handle),
+ }
+ }
+
+ /// Signals this proxy to stop accepting new connections and waits
+ /// for all active connection threads to complete.
+ pub fn shutdown(&mut self) {
+ self.running.store(false, Ordering::SeqCst);
+ if let Some(handle) = self.thread_handle.take() {
+ let _ = handle.join();
+ }
+ }
+}
+
+/// The main loop that listens for incoming (plaintext) connections on
+/// `configuration.bind_address:configuration.local_port`.
+fn run_proxy(configuration: Arc<ProxyConfiguration>, running: Arc<AtomicBool>) {
+ let listener = match TcpListener::bind(format!(
+ "{}:{}",
+ configuration.bind_address, configuration.local_port
+ )) {
+ Ok(l) => l,
+ Err(e) => {
+ error!("Failed to bind to port {}: {}", configuration.local_port, e);
+ return;
+ }
+ };
+ listener.set_nonblocking(true).unwrap();
+
+ info!(
+ "{} proxy listening on port {}:{}",
+ configuration.protocol, configuration.bind_address, configuration.local_port
+ );
+
+ // Keep track of active connections so we can join them on shutdown
+ let mut active_threads = vec![];
+
+ while running.load(Ordering::SeqCst) {
+ match listener.accept() {
+ Ok((stream, addr)) => {
+ info!("New {} connection from {}", configuration.protocol, addr);
+
+ let configuration_clone = Arc::clone(&configuration);
+ let handle = spawn(move || {
+ handle_client(stream, configuration_clone);
});
+ active_threads.push(handle);
+ }
+ Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
+ // No pending connection; sleep briefly then loop again
+ sleep(Duration::from_millis(100));
+ continue;
}
Err(e) => {
- eprintln!("Failed to accept connection: {}", e);
+ error!("Error accepting connection: {}", e);
+ break;
}
}
+
+ // Clean up any finished threads
+ active_threads.retain(|thread| !thread.is_finished());
+
+ // Potential Improvement: Configure thread limit.
+ if active_threads.len() >= 50 {
+ sleep(Duration::from_millis(100));
+ }
+ }
+
+ // On shutdown, wait for all threads to finish
+ for thread in active_threads {
+ let _ = thread.join();
}
}
-fn handle_client(mut client_stream: TcpStream, configuration: Arc<ProxyConfiguration>) {
- let connector = TlsConnector::new().unwrap();
- let remote_stream = TcpStream::connect(format!(
+/// Handles a single client connection by bridging it (plaintext) to a TLS connection.
+fn handle_client(client_stream: TcpStream, configuration: Arc<ProxyConfiguration>) {
+ if let Err(e) = client_stream.set_nonblocking(true) {
+ error!("Failed to set client stream to nonblocking: {}", e);
+ return;
+ }
+
+ let connector = match TlsConnector::new() {
+ Ok(c) => c,
+ Err(e) => {
+ error!("Failed to create TLS connector: {}", e);
+ return;
+ }
+ };
+
+ let remote_addr = format!(
"{}:{}",
- configuration.remote_domain, configuration.remote_port
- ))
- .unwrap();
- let mut remote_stream = connector
- .connect(&configuration.remote_domain, remote_stream)
- .unwrap();
-
- let mut client_stream_clone = client_stream.try_clone().unwrap();
- let mut remote_stream_clone = remote_stream.get_ref().try_clone().unwrap();
-
- let cloned_configuration = Arc::clone(&configuration);
- spawn(move || {
- forward_stream(&mut client_stream, &mut remote_stream, cloned_configuration);
- });
- forward_stream(
- &mut remote_stream_clone,
- &mut client_stream_clone,
- configuration,
+ configuration.remote_host, configuration.remote_port
);
-}
+ let tcp_stream = match TcpStream::connect(&remote_addr) {
+ Ok(stream) => stream,
+ Err(e) => {
+ error!("Failed to connect to {}: {}", remote_addr, e);
+ return;
+ }
+ };
+
+ let tls_stream = match connector.connect(&configuration.remote_host, tcp_stream) {
+ Ok(tls_stream) => tls_stream,
+ Err(e) => {
+ error!(
+ "TLS handshake to {} failed: {}",
+ configuration.remote_host, e
+ );
+ return;
+ }
+ };
+
+ // The nonblocking needs to be set AFTER the TLS handshake is completed.
+ // Otherwise the TLS handshake is interrupted.
+ if let Err(e) = tls_stream.get_ref().set_nonblocking(true) {
+ error!("Failed to set remote stream to nonblocking: {}", e);
+ return;
+ }
+
+ let tls_stream = Arc::new(Mutex::new(tls_stream));
+
+ let client_stream_clone = match client_stream.try_clone() {
+ Ok(s) => s,
+ Err(e) => {
+ error!("Failed to clone client stream: {}", e);
+ return;
+ }
+ };
+
+ // Client to Server Thread
+ let tls_stream_clone = Arc::clone(&tls_stream);
+ let client_to_server = spawn(move || {
+ debug!(">>> BEGIN");
+ let mut buffer = [0u8; 8192];
+ let mut client_reader = client_stream;
+ loop {
+ debug!(">");
+ let bytes_read = match client_reader.read(&mut buffer) {
+ Ok(0) => break,
+ Ok(n) => n,
+ Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
+ sleep(Duration::from_millis(10));
+ continue;
+ }
+ Err(error) => {
+ debug!(">>> Error reading buffer {error}");
+ break;
+ }
+ };
-fn forward_stream<R: Read, W: Write>(
- from: &mut R,
- to: &mut W,
- configuration: Arc<ProxyConfiguration>,
-) {
- let mut buffer = [0; 4096];
- loop {
- match from.read(&mut buffer) {
- Ok(0) => break, // EOF
- Ok(n) => {
- if let Err(e) = to.write_all(&buffer[..n]) {
- eprintln!("{} proxy write error: {}", configuration.protocol, e);
+ let debug_str = String::from_utf8_lossy(&buffer[..bytes_read])
+ .replace('\n', "\\n")
+ .replace('\r', "\\r")
+ .replace('\t', "\\t");
+ debug!(">>> {}", debug_str);
+
+ // Lock the TLS stream and write the data to server
+ match tls_stream_clone.lock() {
+ Ok(mut tls_guard) => {
+ if let Err(error) = tls_guard.write_all(&buffer[..bytes_read]) {
+ debug!(">>> Error writing to server: {error}");
+ break;
+ }
+ if let Err(error) = tls_guard.flush() {
+ debug!(">>> Error flushing server connection: {error}");
+ break;
+ }
+ }
+ Err(error) => {
+ debug!(">>> Error acquiring TLS stream lock: {error}");
break;
}
- if let Err(e) = to.flush() {
- eprintln!("{} proxy flush error: {}", configuration.protocol, e);
+ }
+ }
+ });
+
+ // Server to Client Thread
+ let tls_stream_clone = Arc::clone(&tls_stream);
+ let server_to_client = spawn(move || {
+ debug!("<<< BEGIN");
+ let mut buffer = [0u8; 8192];
+ let mut client_writer = client_stream_clone;
+ loop {
+ debug!("<");
+ // Lock the TLS stream and read from the server
+ let bytes_read = match tls_stream_clone.lock() {
+ Ok(mut tls_guard) => match tls_guard.read(&mut buffer) {
+ Ok(0) => break, // TLS server closed
+ Ok(n) => n,
+ Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
+ sleep(Duration::from_millis(10));
+ continue;
+ }
+ Err(error) => {
+ debug!("<<< Error reading buffer {error}");
+ break;
+ }
+ },
+ Err(error) => {
+ debug!("<<< Error Cloning TLS {error}");
break;
}
+ };
+
+ let debug_str = String::from_utf8_lossy(&buffer[..bytes_read])
+ .replace('\n', "\\n")
+ .replace('\r', "\\r")
+ .replace('\t', "\\t");
+ debug!("<<< {}", debug_str);
+
+ // Write decrypted data to client
+ if client_writer.write_all(&buffer[..bytes_read]).is_err() {
+ debug!("<<< ERR");
+ break;
}
- Err(e) => {
- eprintln!("{} proxy read error: {}", configuration.protocol, e);
+ if client_writer.flush().is_err() {
+ debug!("<<< ERR");
break;
}
}
- }
+ });
+
+ // Wait for both directions to finish
+ let _ = client_to_server.join();
+ let _ = server_to_client.join();
}