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