Skip to main content

eggress_protocol_reverse/
lib.rs

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