Skip to main content

brain_edge/wire_proxy/
mod.rs

1//! wire_proxy — the transparent Brain wire-protocol proxy (deployment Option X,
2//! Shape B: opaque frame-splice).
3//!
4//! A customer's SDK speaks the **binary wire protocol** to the edge with its own
5//! Brain key. The proxy terminates that TCP connection, opens **one** upstream
6//! connection to Brain per customer connection, and splices frames between them
7//! **unchanged** — forwarding the customer's own HELLO / AUTH so Brain
8//! authenticates the connection as that same credential and returns the real
9//! WELCOME / AUTH_OK. The proxy never synthesizes a handshake and never resolves
10//! a key to an identity, so it stays entirely out of the isolation TCB: isolation
11//! lives 100% in Brain (see `tenancy_industry_scale.md` §11).
12//!
13//! What the edge adds on the path is **metering** and **rate-limiting**, both
14//! driven off the 32-byte frame header alone (opcode + stream_id). The single
15//! exception is the handshake AUTH frame, whose CBOR payload is parsed **once**
16//! to sniff the credential that keys the rate-limiter and the metering label —
17//! nothing else is ever decoded. Frames (and their stream_ids — client op streams
18//! must stay odd) are forwarded byte-for-byte, so the future
19//! `agent → space / context → session` wire rename is invisible here.
20//!
21//! Failure modes surface as **wire ERROR frames**, not dropped sockets: a
22//! rate-limited op is answered with a `RateLimited` error on its own stream, and
23//! an unreachable Brain is answered with a `ShardUnavailable` error on the
24//! handshake stream — in both cases the customer's SDK sees a clean structured
25//! error instead of a reset.
26
27mod rate_limit;
28
29pub use rate_limit::{RateLimitConfig, RateLimiters};
30
31use std::net::SocketAddr;
32use std::sync::Arc;
33
34use brain_db_sdk::transport::{read_frame, write_frame};
35use brain_db_sdk::wire::cbor::{from_cbor_bytes, to_cbor_bytes};
36use brain_db_sdk::wire::frame::{FLAG_EOS, Frame};
37use brain_db_sdk::wire::opcode::Opcode;
38use brain_db_sdk::wire::types::{
39    AuthCredentials, AuthPayload, ErrorCategoryWire, ErrorCodeWire, ErrorResponse,
40};
41use tokio::io::AsyncWriteExt;
42use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
43use tokio::net::{TcpListener, TcpStream};
44use tokio::sync::Mutex;
45use tracing::{debug, info, warn};
46
47use crate::port::{MeterEvent, MeteringSink, Outcome};
48
49/// Deploy-time configuration for the wire proxy listener.
50#[derive(Clone, Debug)]
51pub struct WireProxyConfig {
52    /// Address the proxy accepts customer wire connections on.
53    pub wire_listen_addr: SocketAddr,
54    /// The Brain database listener each customer connection is spliced to.
55    pub brain_addr: SocketAddr,
56    /// Per-credential token-bucket rate limiting (disabled when capacity is 0).
57    pub rate: RateLimitConfig,
58}
59
60/// Bind the wire-proxy listener and serve customer connections until the process
61/// exits. One task per accepted connection; a bad accept is logged and skipped
62/// rather than tearing the listener down.
63///
64/// # Errors
65/// Returns the bind error if `wire_listen_addr` can't be bound.
66pub async fn serve(config: WireProxyConfig, meter: Arc<dyn MeteringSink>) -> std::io::Result<()> {
67    let listener = TcpListener::bind(config.wire_listen_addr).await?;
68    info!(
69        addr = %config.wire_listen_addr,
70        brain = %config.brain_addr,
71        "wire proxy listening"
72    );
73    let limiters = Arc::new(RateLimiters::new(config.rate));
74
75    loop {
76        let (sock, peer) = match listener.accept().await {
77            Ok(v) => v,
78            Err(e) => {
79                warn!(error = %e, "wire proxy accept failed");
80                continue;
81            }
82        };
83        let config = config.clone();
84        let meter = Arc::clone(&meter);
85        let limiters = Arc::clone(&limiters);
86        tokio::spawn(async move {
87            debug!(%peer, "wire proxy connection opened");
88            handle_conn(sock, config, meter, limiters).await;
89            debug!(%peer, "wire proxy connection closed");
90        });
91    }
92}
93
94/// Splice one customer connection to a fresh upstream Brain connection.
95///
96/// Opens the 1:1 upstream first (so an unreachable Brain is reported as a wire
97/// ERROR before any splicing), then runs two directional pumps: whichever ends
98/// first (half-close or peer drop) cancels the other.
99async fn handle_conn(
100    customer: TcpStream,
101    config: WireProxyConfig,
102    meter: Arc<dyn MeteringSink>,
103    limiters: Arc<RateLimiters>,
104) {
105    let _ = customer.set_nodelay(true);
106    let (cust_rd, cust_wr) = customer.into_split();
107    // The customer write half is shared: the b→c pump writes Brain's responses,
108    // and the c→b pump writes ERROR frames on rate-limit. A mutex serializes them
109    // so frames never interleave on the wire.
110    let cust_wr = Arc::new(Mutex::new(cust_wr));
111
112    let brain = match TcpStream::connect(config.brain_addr).await {
113        Ok(b) => b,
114        Err(e) => {
115            warn!(brain = %config.brain_addr, error = %e, "wire proxy: brain unreachable");
116            // Report on the handshake stream (0) so the SDK surfaces a clean
117            // "unavailable" during connect instead of a bare socket reset.
118            let mut w = cust_wr.lock().await;
119            let _ = write_frame(
120                &mut *w,
121                &error_frame(
122                    0,
123                    ErrorCodeWire::ShardUnavailable,
124                    ErrorCategoryWire::Unavailable,
125                    "brain upstream unreachable",
126                ),
127            )
128            .await;
129            let _ = w.shutdown().await;
130            return;
131        }
132    };
133    let _ = brain.set_nodelay(true);
134    let (brain_rd, brain_wr) = brain.into_split();
135
136    tokio::select! {
137        () = pump_customer_to_brain(cust_rd, brain_wr, Arc::clone(&cust_wr), &meter, &limiters) => {}
138        () = pump_brain_to_customer(brain_rd, Arc::clone(&cust_wr)) => {}
139    }
140}
141
142/// Brain → customer: a pure relay. Every response/event/heartbeat frame is
143/// forwarded verbatim; the proxy neither meters nor inspects this direction.
144async fn pump_brain_to_customer(mut brain_rd: OwnedReadHalf, cust_wr: Arc<Mutex<OwnedWriteHalf>>) {
145    let mut buf = Vec::new();
146    loop {
147        match read_frame(&mut brain_rd, &mut buf).await {
148            Ok(frame) => {
149                let mut w = cust_wr.lock().await;
150                if write_frame(&mut *w, &frame).await.is_err() {
151                    return;
152                }
153            }
154            // Closed / corrupt / EOF: end this direction, which cancels the other.
155            Err(_) => return,
156        }
157    }
158}
159
160/// Customer → Brain: sniff AUTH once, meter + rate-limit op frames off the
161/// header, then forward every frame unchanged.
162async fn pump_customer_to_brain(
163    mut cust_rd: OwnedReadHalf,
164    mut brain_wr: OwnedWriteHalf,
165    cust_wr: Arc<Mutex<OwnedWriteHalf>>,
166    meter: &Arc<dyn MeteringSink>,
167    limiters: &Arc<RateLimiters>,
168) {
169    let mut buf = Vec::new();
170    // The sniffed credential (rate-limit bucket key) and its non-secret metering
171    // label. Set once, when the AUTH frame passes through.
172    let mut credential: Option<String> = None;
173    let mut label: Option<String> = None;
174
175    loop {
176        let Ok(frame) = read_frame(&mut cust_rd, &mut buf).await else {
177            return;
178        };
179
180        // Handshake sniff — the ONLY place a payload is decoded, and only the AUTH
181        // frame, to key the rate-limiter + metering label on the customer's own
182        // credential. Every other frame is handled header-only.
183        if credential.is_none() && frame.opcode == Opcode::Auth.as_u16() {
184            if let Some(token) = sniff_credential(&frame.payload) {
185                label = Some(fingerprint(&token));
186                credential = Some(token);
187            }
188        }
189
190        // Meter + rate-limit real ops only; handshake/keepalive frames pass
191        // through untouched and uncounted.
192        if !is_control_frame(frame.opcode) {
193            if let Some(cred) = &credential {
194                if !limiters.allow(cred) {
195                    // Answer on the op's own stream and do NOT forward — the
196                    // socket stays up and the SDK sees a structured error.
197                    let ef = error_frame(
198                        frame.stream_id,
199                        ErrorCodeWire::RateLimited,
200                        ErrorCategoryWire::ResourceExhausted,
201                        "rate limit exceeded",
202                    );
203                    {
204                        let mut w = cust_wr.lock().await;
205                        if write_frame(&mut *w, &ef).await.is_err() {
206                            return;
207                        }
208                    }
209                    record(meter, label.as_deref(), frame.opcode, Outcome::Err);
210                    continue;
211                }
212            }
213            // Opaque proxy: we can't cheaply correlate the eventual per-stream
214            // response, so this meters admission, not success.
215            record(meter, label.as_deref(), frame.opcode, Outcome::Ok);
216        }
217
218        // Forward byte-for-byte (stream_id and payload untouched). Frame::encode
219        // reproduces identical bytes, so re-encoding after decode is loss-free.
220        if write_frame(&mut brain_wr, &frame).await.is_err() {
221            return;
222        }
223    }
224}
225
226/// Record one metered op via the injected sink.
227fn record(meter: &Arc<dyn MeteringSink>, tenant: Option<&str>, opcode: u16, outcome: Outcome) {
228    meter.record(&MeterEvent {
229        tenant,
230        op: op_name(opcode),
231        outcome,
232    });
233}
234
235/// Parse the AUTH payload — the single sanctioned payload decode — to recover the
236/// credential the connection presents. Token creds yield the raw token (the same
237/// bytes Brain keys identity on); mTLS yields the asserted subject. Returns `None`
238/// if the payload doesn't decode (a malformed AUTH the proxy simply forwards for
239/// Brain to reject).
240fn sniff_credential(payload: &[u8]) -> Option<String> {
241    let auth: AuthPayload = from_cbor_bytes(payload).ok()?;
242    match auth.credentials {
243        AuthCredentials::Token(bytes) => Some(String::from_utf8_lossy(&bytes).into_owned()),
244        AuthCredentials::Mtls(claim) => Some(claim.asserted_subject),
245    }
246}
247
248/// A stable, non-secret label for a credential, so the raw Brain key never lands
249/// in metering records or logs. Not a security primitive — just a de-identifier.
250fn fingerprint(credential: &str) -> String {
251    use std::hash::{Hash, Hasher};
252    let mut h = std::collections::hash_map::DefaultHasher::new();
253    credential.hash(&mut h);
254    format!("cred-{:016x}", h.finish())
255}
256
257/// Build a terminal (EOS) wire ERROR frame on `stream_id`.
258fn error_frame(
259    stream_id: u32,
260    code: ErrorCodeWire,
261    category: ErrorCategoryWire,
262    message: &str,
263) -> Frame {
264    let payload = to_cbor_bytes(&ErrorResponse {
265        code,
266        category,
267        message: message.to_string(),
268        details: None,
269        retry_after_ms: None,
270    });
271    Frame::new(Opcode::Error.as_u16(), FLAG_EOS, stream_id, payload)
272}
273
274/// Handshake and keepalive frames the proxy neither meters nor rate-limits.
275fn is_control_frame(opcode: u16) -> bool {
276    opcode == Opcode::Hello.as_u16()
277        || opcode == Opcode::Auth.as_u16()
278        || opcode == Opcode::Bye.as_u16()
279        || opcode == Opcode::Ping.as_u16()
280        || opcode == Opcode::ClientPong.as_u16()
281}
282
283/// A coarse verb name for a request opcode, for the metering label. Unknown
284/// opcodes (including any future addition) fall back to `"op"` — the proxy never
285/// needs to know the full verb set to forward a frame.
286fn op_name(opcode: u16) -> &'static str {
287    match opcode {
288        o if o == Opcode::EncodeReq.as_u16() => "encode",
289        o if o == Opcode::EncodeVectorDirectReq.as_u16() => "encode_vector",
290        o if o == Opcode::RecallReq.as_u16() => "recall",
291        o if o == Opcode::PlanReq.as_u16() => "plan",
292        o if o == Opcode::ReasonReq.as_u16() => "reason",
293        o if o == Opcode::ForgetReq.as_u16() => "forget",
294        o if o == Opcode::LinkReq.as_u16() => "link",
295        o if o == Opcode::UnlinkReq.as_u16() => "unlink",
296        o if o == Opcode::MemoryListReq.as_u16() => "memory_list",
297        o if o == Opcode::MemoryInspectReq.as_u16() => "memory_inspect",
298        o if o == Opcode::SubscribeReq.as_u16() => "subscribe",
299        o if o == Opcode::UnsubscribeReq.as_u16() => "unsubscribe",
300        o if o == Opcode::TxnBegin.as_u16() => "txn_begin",
301        o if o == Opcode::TxnCommit.as_u16() => "txn_commit",
302        o if o == Opcode::TxnAbort.as_u16() => "txn_abort",
303        o if o == Opcode::GetCapabilitiesReq.as_u16() => "get_capabilities",
304        _ => "op",
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use brain_db_sdk::wire::types::{AuthMethod, MtlsClaim};
312
313    #[test]
314    fn sniffs_a_token_credential() {
315        let payload = to_cbor_bytes(&AuthPayload {
316            method: AuthMethod::Token,
317            credentials: AuthCredentials::Token(b"secret-key".to_vec()),
318        });
319        assert_eq!(sniff_credential(&payload).as_deref(), Some("secret-key"));
320    }
321
322    #[test]
323    fn sniffs_an_mtls_subject() {
324        let payload = to_cbor_bytes(&AuthPayload {
325            method: AuthMethod::Mtls,
326            credentials: AuthCredentials::Mtls(MtlsClaim {
327                cert_fingerprint: [7u8; 32],
328                asserted_subject: "spiffe://acme/edge".into(),
329            }),
330        });
331        assert_eq!(
332            sniff_credential(&payload).as_deref(),
333            Some("spiffe://acme/edge")
334        );
335    }
336
337    #[test]
338    fn malformed_auth_payload_sniffs_to_none() {
339        assert_eq!(sniff_credential(b"not-cbor-at-all"), None);
340    }
341
342    #[test]
343    fn fingerprint_is_stable_and_hides_the_raw_key() {
344        let a = fingerprint("super-secret");
345        let b = fingerprint("super-secret");
346        assert_eq!(a, b, "same credential → same label");
347        assert!(!a.contains("super-secret"), "raw key is never exposed");
348        assert_ne!(fingerprint("other"), a);
349    }
350
351    #[test]
352    fn control_frames_are_not_metered() {
353        for op in [
354            Opcode::Hello,
355            Opcode::Auth,
356            Opcode::Bye,
357            Opcode::Ping,
358            Opcode::ClientPong,
359        ] {
360            assert!(is_control_frame(op.as_u16()), "{op:?} is control");
361        }
362        assert!(!is_control_frame(Opcode::EncodeReq.as_u16()));
363        assert!(!is_control_frame(Opcode::RecallReq.as_u16()));
364    }
365
366    #[test]
367    fn op_names_cover_the_common_verbs_and_default() {
368        assert_eq!(op_name(Opcode::EncodeReq.as_u16()), "encode");
369        assert_eq!(op_name(Opcode::RecallReq.as_u16()), "recall");
370        assert_eq!(op_name(0xFFFF), "op");
371    }
372
373    #[test]
374    fn error_frame_round_trips_as_a_terminal_error() {
375        let f = error_frame(
376            5,
377            ErrorCodeWire::RateLimited,
378            ErrorCategoryWire::ResourceExhausted,
379            "rate limit exceeded",
380        );
381        assert_eq!(f.opcode, Opcode::Error.as_u16());
382        assert_eq!(f.stream_id, 5);
383        assert!(f.flags & FLAG_EOS != 0, "error frame terminates the stream");
384        let decoded: ErrorResponse = from_cbor_bytes(&f.payload).expect("decode error payload");
385        assert_eq!(decoded.code, ErrorCodeWire::RateLimited);
386        assert_eq!(decoded.message, "rate limit exceeded");
387    }
388}