Author: Kevin Schoon [me@kevinschoon.com]
Hash: a52140f1e56ca92e77f6d95ff811c55490abcaf9
Timestamp: Mon, 23 Sep 2024 21:31:21 +0000 (3 weeks ago)

+68 -34 +/-11 browse
add support for HAProxy's PROXY Protocol
1diff --git a/Cargo.lock b/Cargo.lock
2index 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
27index 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
46index 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
58index 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
73index 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
84deleted file mode 100644
85index 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
110index 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
141index 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
155index 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
167index 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
178index 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()