erbridge_core/reverse/mod.rs
1//! Reverse tunnel: A (`serve`) listens for an external client's traffic and
2//! for B (`connect`) to dial in; once B is connected, A multiplexes every
3//! external connection as a yamux stream over the single A<->B link, and B
4//! dials the locally-configured target for each stream it receives.
5//!
6//! Wire protocol over the TLS-wrapped control connection:
7//! 1. B writes a length-prefixed token frame; A replies with one byte,
8//! `1` (accepted) or `0` (rejected, connection then closes).
9//! 2. Both sides start yamux on the same connection: A as `Mode::Client`
10//! (it is the side that opens new streams), B as `Mode::Server`.
11//! 3. For every new external client, A opens a yamux stream and writes a
12//! length-prefixed tunnel-name frame identifying which `[[connect.tunnel]]`
13//! entry on B should receive it; B reads that frame, resolves the target,
14//! and dials it.
15//! 4. B replies with one byte, `1` (target connected) or `0` (dial failed),
16//! and the rest of the stream is then piped unmodified. B must send this
17//! before any payload: it is what carries yamux's stream ACK, and without
18//! it a target that waits for the client to speak first would leave
19//! streams unacknowledged and cap the tunnel at yamux's 256-stream ack
20//! backlog.
21//!
22//! Only A's *response* direction waits for that byte, because it has to be
23//! consumed rather than delivered to the external client. A forwards the
24//! client's request as soon as it arrives; gating that direction too would put
25//! a full A<->B round trip in front of every new connection.
26
27pub mod connect;
28pub mod enroll;
29pub mod serve;
30pub mod tenant_serve;
31
32use std::io;
33
34use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
35
36pub(crate) const MAX_TOKEN_LEN: usize = 4096;
37const MAX_NAME_LEN: usize = 256;
38
39pub(crate) async fn write_frame<W: AsyncWrite + Unpin>(w: &mut W, data: &[u8]) -> io::Result<()> {
40 let len: u16 = data
41 .len()
42 .try_into()
43 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))?;
44 w.write_all(&len.to_be_bytes()).await?;
45 w.write_all(data).await?;
46 w.flush().await
47}
48
49pub(crate) async fn read_frame<R: AsyncRead + Unpin>(
50 r: &mut R,
51 max_len: usize,
52) -> io::Result<Vec<u8>> {
53 let mut len_buf = [0u8; 2];
54 r.read_exact(&mut len_buf).await?;
55 let len = u16::from_be_bytes(len_buf) as usize;
56 if len > max_len {
57 return Err(io::Error::new(
58 io::ErrorKind::InvalidData,
59 "frame too large",
60 ));
61 }
62 let mut buf = vec![0u8; len];
63 r.read_exact(&mut buf).await?;
64 Ok(buf)
65}
66
67/// Compares two byte strings in time proportional only to their length, not
68/// to the position of the first mismatch, so a failed token check does not
69/// leak timing information about how much of the token was guessed correctly.
70pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
71 if a.len() != b.len() {
72 return false;
73 }
74 let mut diff = 0u8;
75 for (x, y) in a.iter().zip(b.iter()) {
76 diff |= x ^ y;
77 }
78 diff == 0
79}