Skip to main content

ytsaurus_rpc/bus/
mod.rs

1//! Layer 1: bus, the TCP transport.
2//!
3//! [`packet`] is the sans-io half — pure byte↔struct functions with no `async`
4//! anywhere. This module is the thin I/O edge that owns a socket, performs the
5//! handshake and reads and writes whole packets.
6
7pub mod packet;
8
9use bytes::{Bytes, BytesMut};
10use prost::Message as _;
11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12use tokio::net::TcpStream;
13use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
14
15use crate::error::{Error, Result};
16use crate::guid::Guid;
17use crate::proto;
18use packet::{Packet, PacketFlags, PacketType};
19
20/// The four bytes in front of a serialized `THandshake`, spelling "bush" on the
21/// wire — `handshakeSignature` in `yt/go/bus/bus.go`.
22pub const HANDSHAKE_SIGNATURE: u32 = 0x6873_7562;
23
24/// The packet id both sides use for the handshake: the GUID whose first word is
25/// 1 and whose rest is zero.
26fn handshake_packet_id() -> Guid {
27    Guid::from_parts([1, 0, 0, 0])
28}
29
30/// `EEncryptionMode`. Only `Disabled` is implemented — TLS is a later feature,
31/// and a peer that *requires* encryption is refused rather than silently
32/// downgraded.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(i32)]
35pub enum EncryptionMode {
36    Disabled = 0,
37    Optional = 1,
38    Required = 2,
39}
40
41/// How long connecting and completing the handshake may take.
42///
43/// A proxy that accepts a connection and then never speaks is otherwise
44/// indistinguishable from a slow one, and would park the caller for ever: the
45/// handshake is a plain read with nothing above it to impose a deadline.
46pub const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
47
48/// How many bytes of one packet the connection will accept.
49///
50/// The protocol allows 1 GB per part; this is a much lower default so a corrupt
51/// or hostile length word cannot make the client read unbounded input. A caller
52/// reading genuinely large rowsets can raise it.
53///
54/// This bounds the **wire bytes**, which is not the same as the memory
55/// receiving them costs: a part is 12 bytes of header on the wire and about 44
56/// once decoded. `packet::DEFAULT_MAX_PART_COUNT` is what bounds that
57/// multiplier, and the two ceilings are needed together.
58pub const DEFAULT_MAX_MESSAGE_SIZE: u64 = 512 * 1024 * 1024;
59
60/// One TCP connection speaking bus, after a successful handshake.
61///
62/// Reading and writing are separate halves so the connection actor can own one
63/// in each direction without a lock.
64#[derive(Debug)]
65pub struct Bus {
66    pub reader: BusReader,
67    pub writer: BusWriter,
68    /// The connection id sent in our handshake, for diagnostics.
69    pub connection_id: Guid,
70}
71
72#[derive(Debug)]
73pub struct BusReader {
74    stream: OwnedReadHalf,
75    buffer: BytesMut,
76    max_message_size: u64,
77    max_part_count: u32,
78}
79
80#[derive(Debug)]
81pub struct BusWriter {
82    stream: OwnedWriteHalf,
83    buffer: BytesMut,
84}
85
86impl Bus {
87    /// Connects and completes the handshake.
88    pub async fn connect(address: &str) -> Result<Self> {
89        Self::connect_with(address, DEFAULT_MAX_MESSAGE_SIZE, DEFAULT_CONNECT_TIMEOUT).await
90    }
91
92    /// Connects with an explicit packet-size ceiling and connect deadline.
93    ///
94    /// The deadline covers the TCP connect *and* the handshake, because a peer
95    /// that accepts and then says nothing is the case a connect timeout alone
96    /// would miss.
97    pub async fn connect_with(
98        address: &str,
99        max_message_size: u64,
100        connect_timeout: std::time::Duration,
101    ) -> Result<Self> {
102        tokio::time::timeout(
103            connect_timeout,
104            Self::connect_inner(address, max_message_size),
105        )
106        .await
107        .map_err(|_| Error::Connect {
108            address: address.to_owned(),
109            source: std::io::Error::new(
110                std::io::ErrorKind::TimedOut,
111                format!("no handshake within {connect_timeout:?}"),
112            ),
113        })?
114    }
115
116    async fn connect_inner(address: &str, max_message_size: u64) -> Result<Self> {
117        let stream = TcpStream::connect(address)
118            .await
119            .map_err(|source| Error::Connect {
120                address: address.to_owned(),
121                source,
122            })?;
123        // Bus is a request/response protocol over long-lived connections;
124        // Nagle would add up to 40 ms to a small request whose whole point is
125        // latency.
126        stream.set_nodelay(true)?;
127
128        let (read_half, write_half) = stream.into_split();
129        let mut bus = Self {
130            reader: BusReader {
131                stream: read_half,
132                buffer: BytesMut::with_capacity(64 * 1024),
133                max_message_size,
134                max_part_count: packet::DEFAULT_MAX_PART_COUNT,
135            },
136            writer: BusWriter {
137                stream: write_half,
138                buffer: BytesMut::with_capacity(64 * 1024),
139            },
140            connection_id: Guid::random(),
141        };
142        bus.handshake().await?;
143        Ok(bus)
144    }
145
146    /// Sends our handshake and reads the peer's.
147    ///
148    /// The client speaks first. Both sides send a *message* packet whose single
149    /// part is the handshake signature followed by a serialized `THandshake`.
150    async fn handshake(&mut self) -> Result<()> {
151        let handshake = proto::bus::THandshake {
152            connection_id: self.connection_id.to_proto(),
153            encryption_mode: Some(EncryptionMode::Disabled as i32),
154            ..Default::default()
155        };
156
157        let mut part = Vec::with_capacity(4 + handshake.encoded_len());
158        part.extend_from_slice(&HANDSHAKE_SIGNATURE.to_le_bytes());
159        handshake
160            .encode(&mut part)
161            .expect("a Vec never runs out of room");
162
163        self.writer
164            .send(&Packet::message(
165                handshake_packet_id(),
166                vec![Some(Bytes::from(part))],
167                PacketFlags::NONE,
168            ))
169            .await?;
170
171        let reply = self.reader.receive().await?;
172        if reply.packet_type != PacketType::Message {
173            return Err(Error::Protocol(format!(
174                "handshake reply is a {:?} packet, expected a message",
175                reply.packet_type
176            )));
177        }
178        if reply.id != handshake_packet_id() {
179            return Err(Error::Protocol(format!(
180                "handshake reply has packet id {}, expected {}",
181                reply.id,
182                handshake_packet_id()
183            )));
184        }
185        let [Some(payload)] = reply.parts.as_slice() else {
186            return Err(Error::Protocol(format!(
187                "handshake reply has {} parts, expected exactly one",
188                reply.parts.len()
189            )));
190        };
191        if payload.len() < 4 {
192            return Err(Error::Protocol(
193                "handshake reply is too short to hold its signature".to_owned(),
194            ));
195        }
196        let signature = u32::from_le_bytes(payload[0..4].try_into().unwrap());
197        if signature != HANDSHAKE_SIGNATURE {
198            return Err(Error::Protocol(format!(
199                "handshake reply signature is {signature:#010x}, expected {HANDSHAKE_SIGNATURE:#010x}"
200            )));
201        }
202
203        let peer =
204            proto::bus::THandshake::decode(&payload[4..]).map_err(|source| Error::Decode {
205                message: "THandshake",
206                source,
207            })?;
208        if peer.encryption_mode == Some(EncryptionMode::Required as i32) {
209            return Err(Error::Protocol(
210                "the proxy requires encryption, which this crate does not implement yet".to_owned(),
211            ));
212        }
213
214        Ok(())
215    }
216}
217
218impl BusWriter {
219    /// Writes one packet and flushes it.
220    ///
221    /// A packet too large to represent is refused here rather than written as a
222    /// truncated header, which would desynchronise the connection for good.
223    pub async fn send(&mut self, message: &Packet) -> Result<()> {
224        self.buffer.clear();
225        packet::encode(message, &mut self.buffer)?;
226        self.stream.write_all(&self.buffer).await?;
227        self.stream.flush().await?;
228        Ok(())
229    }
230
231    pub async fn shutdown(&mut self) -> Result<()> {
232        self.stream.shutdown().await?;
233        Ok(())
234    }
235}
236
237impl BusReader {
238    /// Reads until one whole packet is available.
239    pub async fn receive(&mut self) -> Result<Packet> {
240        loop {
241            if let Some(message) =
242                packet::decode_with(&mut self.buffer, self.max_message_size, self.max_part_count)?
243            {
244                return Ok(message);
245            }
246            let read = self.stream.read_buf(&mut self.buffer).await?;
247            if read == 0 {
248                return Err(Error::Io(std::io::Error::new(
249                    std::io::ErrorKind::UnexpectedEof,
250                    "the proxy closed the connection",
251                )));
252            }
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use tokio::net::TcpListener;
261
262    /// A stub that speaks just enough bus to answer a handshake, so the
263    /// handshake can be tested without a cluster.
264    async fn handshake_stub(reply: impl Fn(Packet) -> Option<Packet> + Send + 'static) -> String {
265        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
266        let address = listener.local_addr().unwrap().to_string();
267        tokio::spawn(async move {
268            let (stream, _) = listener.accept().await.unwrap();
269            let (mut read_half, mut write_half) = stream.into_split();
270            let mut buffer = BytesMut::new();
271            loop {
272                match packet::decode(&mut buffer, DEFAULT_MAX_MESSAGE_SIZE) {
273                    Ok(Some(request)) => {
274                        if let Some(response) = reply(request) {
275                            let mut out = BytesMut::new();
276                            packet::encode(&response, &mut out).unwrap();
277                            let _ = write_half.write_all(&out).await;
278                            let _ = write_half.flush().await;
279                        }
280                        return;
281                    }
282                    Ok(None) => {}
283                    Err(_) => return,
284                }
285                if read_half.read_buf(&mut buffer).await.unwrap_or(0) == 0 {
286                    return;
287                }
288            }
289        });
290        address
291    }
292
293    /// The bytes of a handshake reply for a given packet id.
294    fn handshake_bytes(id: Guid) -> Vec<u8> {
295        let handshake = proto::bus::THandshake {
296            connection_id: Guid::random().to_proto(),
297            encryption_mode: Some(0),
298            ..Default::default()
299        };
300        let mut part = Vec::new();
301        part.extend_from_slice(&HANDSHAKE_SIGNATURE.to_le_bytes());
302        handshake.encode(&mut part).unwrap();
303        let reply = Packet::message(id, vec![Some(Bytes::from(part))], PacketFlags::NONE);
304        let mut out = BytesMut::new();
305        packet::encode(&reply, &mut out).unwrap();
306        out.to_vec()
307    }
308
309    fn handshake_reply(mode: EncryptionMode) -> Packet {
310        let handshake = proto::bus::THandshake {
311            connection_id: Guid::random().to_proto(),
312            encryption_mode: Some(mode as i32),
313            ..Default::default()
314        };
315        let mut part = Vec::new();
316        part.extend_from_slice(&HANDSHAKE_SIGNATURE.to_le_bytes());
317        handshake.encode(&mut part).unwrap();
318        Packet::message(
319            handshake_packet_id(),
320            vec![Some(Bytes::from(part))],
321            PacketFlags::NONE,
322        )
323    }
324
325    #[tokio::test]
326    async fn the_client_speaks_first_and_its_handshake_is_well_formed() {
327        let (sender, receiver) = tokio::sync::oneshot::channel();
328        let sender = std::sync::Mutex::new(Some(sender));
329        let address = handshake_stub(move |request| {
330            if let Some(sender) = sender.lock().unwrap().take() {
331                let _ = sender.send(request.clone());
332            }
333            Some(handshake_reply(EncryptionMode::Disabled))
334        })
335        .await;
336
337        Bus::connect(&address)
338            .await
339            .expect("the handshake should succeed");
340
341        let request = receiver.await.unwrap();
342        assert_eq!(request.packet_type, PacketType::Message);
343        assert_eq!(
344            request.id,
345            handshake_packet_id(),
346            "the handshake packet id is 1-0-0-0"
347        );
348        assert_eq!(request.parts.len(), 1);
349
350        let payload = request.parts[0].as_ref().unwrap();
351        // Literals throughout: these are the bytes a proxy matches on, and an
352        // assertion written in terms of the constants would follow them
353        // wherever they went.
354        assert_eq!(&payload[0..4], b"bush", "the signature spells bush");
355        assert_eq!(HANDSHAKE_SIGNATURE, 0x6873_7562);
356        assert_eq!(
357            request.id.0,
358            [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
359            "the handshake packet id is the GUID 1-0-0-0"
360        );
361        let handshake = proto::bus::THandshake::decode(&payload[4..]).unwrap();
362        assert_eq!(handshake.encryption_mode, Some(0), "encryption is disabled");
363    }
364
365    #[tokio::test]
366    async fn a_peer_that_requires_encryption_is_refused_not_downgraded() {
367        let address = handshake_stub(|_| Some(handshake_reply(EncryptionMode::Required))).await;
368        let error = Bus::connect(&address).await.unwrap_err();
369        assert!(
370            error.to_string().contains("requires encryption"),
371            "unexpected error: {error}"
372        );
373    }
374
375    #[tokio::test]
376    async fn a_handshake_with_the_wrong_signature_is_refused() {
377        let address = handshake_stub(|_| {
378            Some(Packet::message(
379                handshake_packet_id(),
380                vec![Some(Bytes::from_static(b"junk-and-more-junk"))],
381                PacketFlags::NONE,
382            ))
383        })
384        .await;
385        let error = Bus::connect(&address).await.unwrap_err();
386        assert!(
387            error.to_string().contains("signature"),
388            "unexpected error: {error}"
389        );
390    }
391
392    #[tokio::test]
393    async fn a_handshake_with_the_wrong_packet_id_is_refused() {
394        let address = handshake_stub(|_| {
395            let mut reply = handshake_reply(EncryptionMode::Disabled);
396            reply.id = Guid::from_parts([7, 0, 0, 0]);
397            Some(reply)
398        })
399        .await;
400        let error = Bus::connect(&address).await.unwrap_err();
401        assert!(
402            error.to_string().contains("packet id"),
403            "unexpected error: {error}"
404        );
405    }
406
407    #[tokio::test]
408    async fn a_closed_connection_is_an_error_not_a_hang() {
409        let address = handshake_stub(|_| None).await;
410        let error = Bus::connect(&address).await.unwrap_err();
411        assert!(
412            error.to_string().contains("closed the connection"),
413            "unexpected error: {error}"
414        );
415    }
416
417    /// A peer that accepts the connection and then says nothing must not park
418    /// the caller for ever.
419    #[tokio::test]
420    async fn a_silent_peer_does_not_hang_the_connect() {
421        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
422        let address = listener.local_addr().unwrap().to_string();
423        // Accept and hold the socket open, without ever replying.
424        let _accepting = tokio::spawn(async move {
425            let _held = listener.accept().await;
426            std::future::pending::<()>().await;
427        });
428
429        let started = std::time::Instant::now();
430        let error = Bus::connect_with(
431            &address,
432            DEFAULT_MAX_MESSAGE_SIZE,
433            std::time::Duration::from_millis(200),
434        )
435        .await
436        .unwrap_err();
437
438        assert!(
439            started.elapsed() < std::time::Duration::from_secs(5),
440            "it waited too long"
441        );
442        assert!(
443            error.to_string().contains("no handshake within"),
444            "unexpected error: {error}"
445        );
446    }
447
448    /// The reader must apply the ceiling it was given.
449    ///
450    /// The limit is tested thoroughly one layer down, where tests hand a bound
451    /// straight to `packet::decode` — but nothing checked that `BusReader`
452    /// passes its own configured value along, and it is the only thing standing
453    /// between a hostile length word and an unbounded reservation.
454    #[tokio::test]
455    async fn the_reader_applies_its_own_size_ceiling() {
456        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
457        let address = listener.local_addr().unwrap().to_string();
458
459        tokio::spawn(async move {
460            let (stream, _) = listener.accept().await.unwrap();
461            let (mut read_half, mut write_half) = stream.into_split();
462            let mut buffer = BytesMut::new();
463            // Answer the handshake, then announce a packet far above the
464            // ceiling this connection was opened with.
465            loop {
466                if let Ok(Some(request)) = packet::decode(&mut buffer, DEFAULT_MAX_MESSAGE_SIZE) {
467                    let _ = write_half.write_all(&handshake_bytes(request.id)).await;
468                    break;
469                }
470                if read_half.read_buf(&mut buffer).await.unwrap_or(0) == 0 {
471                    return;
472                }
473            }
474            let big = Packet::message(
475                Guid::random(),
476                vec![Some(Bytes::from(vec![0u8; 128 * 1024]))],
477                PacketFlags::NONE,
478            );
479            let mut out = BytesMut::new();
480            packet::encode(&big, &mut out).unwrap();
481            let _ = write_half.write_all(&out).await;
482            std::future::pending::<()>().await;
483        });
484
485        // A ceiling below the packet the peer is about to send.
486        let mut bus = Bus::connect_with(&address, 4096, DEFAULT_CONNECT_TIMEOUT)
487            .await
488            .expect("the handshake itself is small");
489        let error = bus.reader.receive().await.unwrap_err();
490        assert!(
491            error.to_string().contains("more than the 4096"),
492            "the reader ignored its ceiling: {error}"
493        );
494    }
495
496    #[tokio::test]
497    async fn connecting_to_a_closed_port_reports_the_address() {
498        // Port 1 on loopback: reserved, and nothing listens there.
499        let error = Bus::connect("127.0.0.1:1").await.unwrap_err();
500        assert!(
501            error.to_string().contains("127.0.0.1:1"),
502            "unexpected error: {error}"
503        );
504    }
505}