Skip to main content

eggress_protocol_reverse/
lib.rs

1use std::time::Duration;
2use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt};
3use tokio::net::TcpStream;
4
5pub mod client;
6pub mod compat_pproxy;
7pub mod metrics;
8pub mod server;
9
10/// Handshake response: accept.
11pub const HANDSHAKE_ACCEPT: u8 = 0x01;
12
13/// Handshake response: reject.
14pub const HANDSHAKE_REJECT: u8 = 0x00;
15
16/// Errors specific to the reverse protocol.
17#[derive(Debug, thiserror::Error)]
18pub enum ProtocolError {
19    #[error("authentication failed")]
20    AuthFailed,
21    #[error("authentication required")]
22    AuthRequired,
23    #[error("connection closed")]
24    ConnectionClosed,
25    #[error("bind address {0} is not in the allow_bind allowlist")]
26    BindDenied(std::net::SocketAddr),
27    #[error("invalid configuration: {0}")]
28    ConfigInvalid(String),
29    #[error("IO error: {0}")]
30    Io(#[from] std::io::Error),
31}
32
33/// State of a reverse control channel.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ControlState {
36    Disconnected,
37    Connecting,
38    Authenticating,
39    Ready,
40    Draining,
41    Closed,
42}
43
44/// Write auth credentials as raw bytes to a stream.
45///
46/// pproxy format: raw `user:pass` string bytes.
47pub async fn write_auth(
48    stream: &mut TcpStream,
49    username: &str,
50    password: &str,
51) -> Result<(), ProtocolError> {
52    let auth = format!("{}:{}\n", username, password);
53    stream.write_all(auth.as_bytes()).await?;
54    stream.flush().await?;
55    Ok(())
56}
57
58/// Read and validate the 1-byte handshake response.
59pub async fn read_handshake(stream: &mut TcpStream) -> Result<(), ProtocolError> {
60    let mut buf = [0u8; 1];
61    stream.read_exact(&mut buf).await?;
62    if buf[0] == HANDSHAKE_REJECT {
63        return Err(ProtocolError::AuthFailed);
64    }
65    Ok(())
66}
67
68/// Write the 1-byte handshake response (accept).
69pub async fn write_handshake_accept(stream: &mut TcpStream) -> Result<(), ProtocolError> {
70    stream.write_all(&[HANDSHAKE_ACCEPT]).await?;
71    Ok(())
72}
73
74/// Write the 1-byte handshake response (reject).
75pub async fn write_handshake_reject(stream: &mut TcpStream) -> Result<(), ProtocolError> {
76    stream.write_all(&[HANDSHAKE_REJECT]).await?;
77    Ok(())
78}
79
80/// Perform the client-side auth handshake: send credentials, read response.
81pub async fn client_auth_handshake(
82    stream: &mut TcpStream,
83    username: &str,
84    password: &str,
85) -> Result<(), ProtocolError> {
86    write_auth(stream, username, password).await?;
87    read_handshake(stream).await
88}
89
90/// Perform the server-side auth handshake: read credentials, validate, respond.
91///
92/// Returns the redacted auth representation `user:****` (never the password)
93/// so callers can log it without leaking credentials. The full raw bytes are
94/// only retained for the duration of the auth phase and then dropped.
95pub async fn server_auth_handshake(
96    stream: &mut TcpStream,
97    expected_user: Option<&str>,
98    expected_pass: Option<&str>,
99) -> Result<String, ProtocolError> {
100    // Read auth bytes (newline-delimited user:pass string).
101    // Cap at 4 KiB to prevent unbounded memory growth from malicious clients.
102    const MAX_AUTH_BYTES: u64 = 4096;
103    let mut auth_buf = Vec::with_capacity(1024);
104    {
105        use tokio::io::AsyncReadExt;
106        let mut limited = (&mut *stream).take(MAX_AUTH_BYTES);
107        let mut reader = tokio::io::BufReader::new(&mut limited);
108        reader.read_until(b'\n', &mut auth_buf).await?;
109    }
110    if auth_buf.is_empty() {
111        return Err(ProtocolError::ConnectionClosed);
112    }
113    if auth_buf.len() > MAX_AUTH_BYTES as usize {
114        return Err(ProtocolError::ConfigInvalid(
115            "auth payload exceeds maximum length".to_string(),
116        ));
117    }
118    // The newline is part of the wire framing. Requiring it prevents a
119    // truncated credential payload from being accepted at EOF.
120    if auth_buf.last() != Some(&b'\n') {
121        return Err(ProtocolError::AuthFailed);
122    }
123    auth_buf.pop();
124
125    let auth_str = String::from_utf8_lossy(&auth_buf).to_string();
126
127    // Validate if credentials are configured
128    if let (Some(exp_user), Some(exp_pass)) = (expected_user, expected_pass) {
129        let (user, pass) = parse_auth_str(&auth_str);
130        use subtle::ConstantTimeEq;
131        let user_ok: bool = user.as_bytes().ct_eq(exp_user.as_bytes()).into();
132        let pass_ok: bool = pass.as_bytes().ct_eq(exp_pass.as_bytes()).into();
133        if !user_ok || !pass_ok {
134            write_handshake_reject(stream).await?;
135            return Err(ProtocolError::AuthFailed);
136        }
137    }
138
139    write_handshake_accept(stream).await?;
140    Ok(redact_auth(&auth_str))
141}
142
143/// Build a redacted form of an auth string suitable for logging.
144///
145/// Replaces the password with `****` while preserving the username. If the
146/// string contains no `:`, the entire content is replaced with `****`.
147pub fn redact_auth(auth: &str) -> String {
148    let (user, _) = parse_auth_str(auth);
149    if user.is_empty() && auth.is_empty() {
150        return String::new();
151    }
152    if !auth.contains(':') {
153        return "****".to_string();
154    }
155    format!("{}:****", user)
156}
157
158/// Parse a `user:pass` auth string.
159fn parse_auth_str(auth: &str) -> (&str, &str) {
160    match auth.find(':') {
161        Some(idx) => (&auth[..idx], &auth[idx + 1..]),
162        None => (auth, ""),
163    }
164}
165
166/// Relay data bidirectionally between two TCP streams.
167///
168/// Takes ownership of both streams and relays until either side closes.
169pub async fn relay_bidirectional(
170    stream_a: TcpStream,
171    stream_b: TcpStream,
172) -> Result<(), ProtocolError> {
173    relay_bidirectional_with_timeout(stream_a, stream_b, None).await
174}
175
176/// Relay data bidirectionally, ending the session when both sides reach
177/// end-of-stream. Neither direction is cancelled when the other observes
178/// EOF — half-close is preserved so the still-open direction can drain
179/// any application bytes the peer already produced.
180pub async fn relay_bidirectional_with_timeout(
181    stream_a: TcpStream,
182    stream_b: TcpStream,
183    idle_timeout: Option<Duration>,
184) -> Result<(), ProtocolError> {
185    let (mut a_read, mut a_write) = tokio::io::split(stream_a);
186    let (mut b_read, mut b_write) = tokio::io::split(stream_b);
187
188    let a_to_b = async {
189        let mut buf = [0u8; 8192];
190        let mut ended = false;
191        loop {
192            let read_result = match idle_timeout {
193                Some(timeout) => match tokio::time::timeout(timeout, a_read.read(&mut buf)).await {
194                    Ok(result) => result,
195                    Err(_) => {
196                        ended = true;
197                        break;
198                    }
199                },
200                None => a_read.read(&mut buf).await,
201            };
202            match read_result {
203                Ok(0) => {
204                    ended = true;
205                    break;
206                }
207                Ok(n) => {
208                    if b_write.write_all(&buf[..n]).await.is_err() {
209                        break;
210                    }
211                }
212                Err(_) => break,
213            }
214        }
215        let _ = b_write.shutdown().await;
216        ended
217    };
218
219    let b_to_a = async {
220        let mut buf = [0u8; 8192];
221        let mut ended = false;
222        loop {
223            let read_result = match idle_timeout {
224                Some(timeout) => match tokio::time::timeout(timeout, b_read.read(&mut buf)).await {
225                    Ok(result) => result,
226                    Err(_) => {
227                        ended = true;
228                        break;
229                    }
230                },
231                None => b_read.read(&mut buf).await,
232            };
233            match read_result {
234                Ok(0) => {
235                    ended = true;
236                    break;
237                }
238                Ok(n) => {
239                    if a_write.write_all(&buf[..n]).await.is_err() {
240                        break;
241                    }
242                }
243                Err(_) => break,
244            }
245        }
246        let _ = a_write.shutdown().await;
247        ended
248    };
249
250    let (a_done, b_done) = tokio::join!(a_to_b, b_to_a);
251    let _ = (a_done, b_done);
252    Ok(())
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn parse_auth_str_normal() {
261        let (user, pass) = parse_auth_str("user:pass");
262        assert_eq!(user, "user");
263        assert_eq!(pass, "pass");
264    }
265
266    #[test]
267    fn parse_auth_str_empty() {
268        let (user, pass) = parse_auth_str("");
269        assert_eq!(user, "");
270        assert_eq!(pass, "");
271    }
272
273    #[test]
274    fn parse_auth_str_no_colon() {
275        let (user, pass) = parse_auth_str("nocolon");
276        assert_eq!(user, "nocolon");
277        assert_eq!(pass, "");
278    }
279
280    #[test]
281    fn parse_auth_str_multiple_colons() {
282        let (user, pass) = parse_auth_str("user:pass:extra");
283        assert_eq!(user, "user");
284        assert_eq!(pass, "pass:extra");
285    }
286
287    #[test]
288    fn redact_auth_basic() {
289        assert_eq!(redact_auth("user:pass"), "user:****");
290    }
291
292    #[test]
293    fn redact_auth_no_colon() {
294        assert_eq!(redact_auth("opaque"), "****");
295    }
296
297    #[test]
298    fn redact_auth_empty() {
299        assert_eq!(redact_auth(""), "");
300    }
301
302    #[test]
303    fn redact_auth_password_contains_colon() {
304        // Multiple colons: only the first separates user/pass
305        assert_eq!(redact_auth("user:p:a:s:s"), "user:****");
306    }
307
308    #[test]
309    fn redact_auth_does_not_leak_password() {
310        let s = redact_auth("user:supersecret123");
311        assert!(!s.contains("supersecret"));
312        assert!(!s.contains("secret"));
313    }
314
315    #[test]
316    fn handshake_constants() {
317        assert_eq!(HANDSHAKE_ACCEPT, 0x01);
318        assert_eq!(HANDSHAKE_REJECT, 0x00);
319    }
320
321    #[test]
322    fn control_state_variants() {
323        let state = ControlState::Disconnected;
324        assert_eq!(state, ControlState::Disconnected);
325
326        let state = ControlState::Connecting;
327        assert_eq!(state, ControlState::Connecting);
328
329        let state = ControlState::Authenticating;
330        assert_eq!(state, ControlState::Authenticating);
331
332        let state = ControlState::Ready;
333        assert_eq!(state, ControlState::Ready);
334
335        let state = ControlState::Draining;
336        assert_eq!(state, ControlState::Draining);
337
338        let state = ControlState::Closed;
339        assert_eq!(state, ControlState::Closed);
340    }
341
342    #[tokio::test]
343    async fn auth_handshake_success() {
344        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
345        let addr = listener.local_addr().unwrap();
346
347        let server = tokio::spawn(async move {
348            let (mut stream, _) = listener.accept().await.unwrap();
349            let result = server_auth_handshake(&mut stream, Some("user"), Some("pass")).await;
350            assert!(result.is_ok());
351            // Returned form is redacted to avoid leaking the password
352            assert_eq!(result.unwrap(), "user:****");
353        });
354
355        let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
356        let result = client_auth_handshake(&mut stream, "user", "pass").await;
357        assert!(result.is_ok());
358
359        server.await.unwrap();
360    }
361
362    #[tokio::test]
363    async fn auth_handshake_failure() {
364        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
365        let addr = listener.local_addr().unwrap();
366
367        let server = tokio::spawn(async move {
368            let (mut stream, _) = listener.accept().await.unwrap();
369            let result = server_auth_handshake(&mut stream, Some("user"), Some("pass")).await;
370            assert!(result.is_err());
371        });
372
373        let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
374        let result = client_auth_handshake(&mut stream, "user", "wrong").await;
375        assert!(result.is_err());
376
377        server.await.unwrap();
378    }
379
380    #[tokio::test]
381    async fn auth_no_credentials_configured() {
382        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
383        let addr = listener.local_addr().unwrap();
384
385        let server = tokio::spawn(async move {
386            let (mut stream, _) = listener.accept().await.unwrap();
387            let result = server_auth_handshake(&mut stream, None, None).await;
388            assert!(result.is_ok());
389        });
390
391        let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
392        let result = client_auth_handshake(&mut stream, "", "").await;
393        assert!(result.is_ok());
394
395        server.await.unwrap();
396    }
397
398    #[tokio::test]
399    async fn relay_bidirectional_data() {
400        let listener_a = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
401        let addr_a = listener_a.local_addr().unwrap();
402        let listener_b = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
403        let addr_b = listener_b.local_addr().unwrap();
404
405        // Spawn connector for side A
406        let conn_a =
407            tokio::spawn(async move { tokio::net::TcpStream::connect(addr_a).await.unwrap() });
408
409        // Spawn connector for side B
410        let conn_b =
411            tokio::spawn(async move { tokio::net::TcpStream::connect(addr_b).await.unwrap() });
412
413        let (stream_a, _) = listener_a.accept().await.unwrap();
414        let (stream_b, _) = listener_b.accept().await.unwrap();
415
416        // Spawn relay
417        let relay_handle = tokio::spawn(async move {
418            let _ = relay_bidirectional(stream_a, stream_b).await;
419        });
420
421        let mut conn_a = conn_a.await.unwrap();
422        let mut conn_b = conn_b.await.unwrap();
423
424        // Write from A to B
425        tokio::io::AsyncWriteExt::write_all(&mut conn_a, b"hello from A")
426            .await
427            .unwrap();
428
429        // Read from B
430        let mut buf = [0u8; 1024];
431        let n = tokio::io::AsyncReadExt::read(&mut conn_b, &mut buf)
432            .await
433            .unwrap();
434        assert_eq!(&buf[..n], b"hello from A");
435
436        // Write from B to A
437        tokio::io::AsyncWriteExt::write_all(&mut conn_b, b"hello from B")
438            .await
439            .unwrap();
440
441        // Read from A
442        let n = tokio::io::AsyncReadExt::read(&mut conn_a, &mut buf)
443            .await
444            .unwrap();
445        assert_eq!(&buf[..n], b"hello from B");
446
447        // Clean up
448        drop(conn_a);
449        drop(conn_b);
450        let _ = relay_handle.await;
451    }
452}