Skip to main content

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}