Commit
+68 -34 +/-11 browse
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() |