Author:
Hash:
Timestamp:
+68 -34 +/-11 browse
Kevin Schoon [me@kevinschoon.com]
a52140f1e56ca92e77f6d95ff811c55490abcaf9
Mon, 23 Sep 2024 21:31:21 +0000 (1.2 years ago)
| 1 | diff --git a/Cargo.lock b/Cargo.lock |
| 2 | index 704ce6f..3164742 100644 |
| 3 | --- a/Cargo.lock |
| 4 | +++ b/Cargo.lock |
| 5 | @@ -1093,6 +1093,7 @@ dependencies = [ |
| 6 | "mail-parser", |
| 7 | "maildir", |
| 8 | "md5", |
| 9 | + "proxy-header", |
| 10 | "rustls 0.23.13", |
| 11 | "rustls-pemfile 2.1.3", |
| 12 | "smtp-proto", |
| 13 | @@ -1326,6 +1327,12 @@ dependencies = [ |
| 14 | ] |
| 15 | |
| 16 | [[package]] |
| 17 | + name = "proxy-header" |
| 18 | + version = "0.1.2" |
| 19 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 20 | + checksum = "dc1493f63ddddfba840c3169e997c2905d09538ace72d64e84af6324c6e0e065" |
| 21 | + |
| 22 | + [[package]] |
| 23 | name = "quick-error" |
| 24 | version = "1.2.3" |
| 25 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 26 | diff --git a/README.md b/README.md |
| 27 | index 2c3a4a0..47a657d 100644 |
| 28 | --- a/README.md |
| 29 | +++ b/README.md |
| 30 | @@ -87,6 +87,14 @@ STARTTLS is supported for upgrading plain text connections (opportunistic TLS). |
| 31 | |
| 32 | Implicit TLS on a dedicated port is a WIP. |
| 33 | |
| 34 | + ### Proxy Protocol |
| 35 | + |
| 36 | + Often times you don't want to bind directly to privileged port `25`. Maitred |
| 37 | + provdes support for HAProxy's |
| 38 | + [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) |
| 39 | + which lets you proxy traffic from Nginx (or HAProxy) and expose the client's |
| 40 | + original source IP address. This is required for SPF session level validation. |
| 41 | + |
| 42 | |
| 43 | ## Attributions |
| 44 | |
| 45 | diff --git a/cmd/maitred-debug/src/config.rs b/cmd/maitred-debug/src/config.rs |
| 46 | index ed9bb84..aac99a0 100644 |
| 47 | --- a/cmd/maitred-debug/src/config.rs |
| 48 | +++ b/cmd/maitred-debug/src/config.rs |
| 49 | @@ -29,5 +29,6 @@ pub(crate) struct Config { |
| 50 | pub spf: Spf, |
| 51 | pub dkim: Dkim, |
| 52 | pub accounts: Vec<Account>, |
| 53 | - pub tls: Option<Tls> |
| 54 | + pub tls: Option<Tls>, |
| 55 | + pub proxy_protocol: Option<bool>, |
| 56 | } |
| 57 | diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs |
| 58 | index 0a12dc2..933b52c 100644 |
| 59 | --- a/cmd/maitred-debug/src/main.rs |
| 60 | +++ b/cmd/maitred-debug/src/main.rs |
| 61 | @@ -88,6 +88,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 62 | session_opts = session_opts.starttls_enabled(true); |
| 63 | } |
| 64 | |
| 65 | + if config.proxy_protocol.is_some_and(|enabled| enabled) { |
| 66 | + mail_server = mail_server.proxy_protocol(true); |
| 67 | + }; |
| 68 | + |
| 69 | mail_server = mail_server.with_session_opts(session_opts); |
| 70 | mail_server.listen().await?; |
| 71 | Ok(()) |
| 72 | diff --git a/contrib/nginx-proxy/README.md b/contrib/nginx-proxy/README.md |
| 73 | index d557bc9..60803c0 100644 |
| 74 | --- a/contrib/nginx-proxy/README.md |
| 75 | +++ b/contrib/nginx-proxy/README.md |
| 76 | @@ -6,6 +6,5 @@ Maitred can be used with Nginx. |
| 77 | ### Usage |
| 78 | |
| 79 | ``` |
| 80 | - python auth_http.py |
| 81 | nginx -p . -c nginx.conf |
| 82 | ``` |
| 83 | diff --git a/contrib/nginx-proxy/auth_http.py b/contrib/nginx-proxy/auth_http.py |
| 84 | deleted file mode 100644 |
| 85 | index 24f37d2..0000000 |
| 86 | --- a/contrib/nginx-proxy/auth_http.py |
| 87 | +++ /dev/null |
| 88 | @@ -1,20 +0,0 @@ |
| 89 | - import socketserver |
| 90 | - |
| 91 | - from http import server |
| 92 | - |
| 93 | - |
| 94 | - class HTTPRequestHandler(server.SimpleHTTPRequestHandler): |
| 95 | - def end_headers(self): |
| 96 | - print(self.headers) |
| 97 | - rcpt_to_header = self.headers['Auth-SMTP-To'] |
| 98 | - rcpt_to = rcpt_to_header.split(":")[1][2:-1] |
| 99 | - self.send_header("Auth-Status", "OK") |
| 100 | - self.send_header("Auth-Server","127.0.0.1") |
| 101 | - self.send_header("Auth-Port", "2525") |
| 102 | - server.SimpleHTTPRequestHandler.end_headers(self) |
| 103 | - |
| 104 | - |
| 105 | - if __name__ == '__main__': |
| 106 | - with socketserver.TCPServer(("127.0.0.1", 30000), HTTPRequestHandler) as httpd: |
| 107 | - print("Listening @ 127.0.0.1:30000") |
| 108 | - httpd.serve_forever() |
| 109 | diff --git a/contrib/nginx-proxy/nginx.conf b/contrib/nginx-proxy/nginx.conf |
| 110 | index 48cb27b..62f5879 100644 |
| 111 | --- a/contrib/nginx-proxy/nginx.conf |
| 112 | +++ b/contrib/nginx-proxy/nginx.conf |
| 113 | @@ -5,16 +5,16 @@ pid /tmp/nginx.pid; |
| 114 | |
| 115 | events {} |
| 116 | |
| 117 | - mail { |
| 118 | - server_name localhost; |
| 119 | - auth_http http://127.0.0.1:30000; |
| 120 | - |
| 121 | - proxy_pass_error_message on; |
| 122 | - xclient off; |
| 123 | - |
| 124 | + # SMTP Layer 4 Proxy |
| 125 | + |
| 126 | + stream { |
| 127 | + log_format basic '$remote_addr [$time_local] ' |
| 128 | + '$protocol $status $bytes_sent $bytes_received ' |
| 129 | + '$session_time'; |
| 130 | + access_log /dev/stderr basic; |
| 131 | server { |
| 132 | - listen 2225; |
| 133 | - protocol smtp; |
| 134 | - smtp_auth none; |
| 135 | + listen 2555; |
| 136 | + proxy_pass 127.0.0.1:2525; |
| 137 | + proxy_protocol on; |
| 138 | } |
| 139 | } |
| 140 | diff --git a/maitred.toml b/maitred.toml |
| 141 | index 8a60c01..46d1033 100644 |
| 142 | --- a/maitred.toml |
| 143 | +++ b/maitred.toml |
| 144 | @@ -4,6 +4,9 @@ maildir = "mail" |
| 145 | # address to bind to |
| 146 | address = "0.0.0.0:2525" |
| 147 | |
| 148 | + # Enable HAProxy's PROXY Protocol |
| 149 | + proxy_protocol = false |
| 150 | + |
| 151 | [dkim] |
| 152 | enabled = false |
| 153 | |
| 154 | diff --git a/maitred/Cargo.toml b/maitred/Cargo.toml |
| 155 | index 40ef577..76c4153 100644 |
| 156 | --- a/maitred/Cargo.toml |
| 157 | +++ b/maitred/Cargo.toml |
| 158 | @@ -15,6 +15,7 @@ mail-builder = "0.3.2" |
| 159 | mail-parser = { version = "0.9.3", features = ["serde", "serde_support"] } |
| 160 | maildir = "0.6.4" |
| 161 | md5 = "0.7.0" |
| 162 | + proxy-header = "0.1.2" |
| 163 | rustls = "0.23.13" |
| 164 | rustls-pemfile = "2.1.3" |
| 165 | smtp-proto = { version = "0.1.5", features = ["serde", "serde_support"] } |
| 166 | diff --git a/maitred/src/error.rs b/maitred/src/error.rs |
| 167 | index b43c80f..fcfc8c6 100644 |
| 168 | --- a/maitred/src/error.rs |
| 169 | +++ b/maitred/src/error.rs |
| 170 | @@ -5,4 +5,6 @@ pub enum Error { |
| 171 | /// An IO related error such as not being able to bind to a TCP socket |
| 172 | #[error("Io: {0}")] |
| 173 | Io(#[from] std::io::Error), |
| 174 | + #[error("Proxy Protocol Error: {0}")] |
| 175 | + ProxyProtocol(#[from] proxy_header::Error) |
| 176 | } |
| 177 | diff --git a/maitred/src/server.rs b/maitred/src/server.rs |
| 178 | index fd943d1..d641090 100644 |
| 179 | --- a/maitred/src/server.rs |
| 180 | +++ b/maitred/src/server.rs |
| 181 | @@ -13,6 +13,7 @@ use futures::SinkExt; |
| 182 | use futures::StreamExt; |
| 183 | use mail_auth::Resolver; |
| 184 | use mail_parser::Message; |
| 185 | + use proxy_header::{ParseConfig, ProxyHeader}; |
| 186 | use smtp_proto::Request; |
| 187 | use tokio::io::BufStream; |
| 188 | use tokio::net::TcpListener; |
| 189 | @@ -100,9 +101,9 @@ pub struct Server { |
| 190 | shutdown_handles: Vec<Sender<bool>>, |
| 191 | dkim_verification: bool, |
| 192 | spf_verification: bool, |
| 193 | - current_addr: Option<SocketAddr>, |
| 194 | resolver: Option<Arc<Mutex<Resolver>>>, |
| 195 | tls_certificates: Option<(PathBuf, PathBuf)>, |
| 196 | + proxy_protocol: bool, |
| 197 | } |
| 198 | |
| 199 | impl Default for Server { |
| 200 | @@ -117,9 +118,9 @@ impl Default for Server { |
| 201 | shutdown_handles: vec![], |
| 202 | dkim_verification: false, |
| 203 | spf_verification: false, |
| 204 | - current_addr: None, |
| 205 | resolver: None, |
| 206 | tls_certificates: None, |
| 207 | + proxy_protocol: false, |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | @@ -185,6 +186,13 @@ impl Server { |
| 212 | self |
| 213 | } |
| 214 | |
| 215 | + /// Enable support for HAProxy's Proxy Protocol |
| 216 | + /// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt |
| 217 | + pub fn proxy_protocol(mut self, enabled: bool) -> Self { |
| 218 | + self.proxy_protocol = enabled; |
| 219 | + self |
| 220 | + } |
| 221 | + |
| 222 | async fn rustls_config(&self) -> Result<rustls::ServerConfig, ServerError> { |
| 223 | let (private_key_path, cert_path) = self |
| 224 | .tls_certificates |
| 225 | @@ -199,6 +207,7 @@ impl Server { |
| 226 | .with_single_cert(certs, private_key)?) |
| 227 | } |
| 228 | |
| 229 | + // TODO: Eliminate duplicated code |
| 230 | async fn serve_tls<T>( |
| 231 | &self, |
| 232 | stream: &mut BufStream<T>, |
| 233 | @@ -467,6 +476,26 @@ impl Server { |
| 234 | let (socket, addr) = listener.accept().await.unwrap(); |
| 235 | let local_addr = socket.local_addr()?; |
| 236 | tracing::info!("Accepted connection on: {:?} from: {:?}", local_addr, addr); |
| 237 | + // pass the proxied address if proxy protocol is enabled |
| 238 | + let addr = if self.proxy_protocol { |
| 239 | + let mut buf: [u8; 512] = [0; 512]; |
| 240 | + socket.peek(&mut buf).await?; |
| 241 | + let (header, len) = ProxyHeader::parse(&buf, ParseConfig::default())?; |
| 242 | + tracing::info!("Parsed proxy protocol header: {:?} bytes={}", header, len); |
| 243 | + if let Some(proxied) = header.proxied_address() { |
| 244 | + // discard the proxy header |
| 245 | + let mut buf = vec![0; len]; |
| 246 | + socket.readable().await?; |
| 247 | + socket.try_read(&mut buf)?; |
| 248 | + proxied.source |
| 249 | + // socket.proxied.source |
| 250 | + } else { |
| 251 | + tracing::error!("Failed to parse proxied address"); |
| 252 | + addr |
| 253 | + } |
| 254 | + } else { |
| 255 | + addr |
| 256 | + }; |
| 257 | let pipelining = self |
| 258 | .options |
| 259 | .as_ref() |