Skip to main content

conmonrs/streaming_server/
remotecommand.rs

1//! The Kubernetes remote command protocols of exec and attach sessions, as
2//! k8s.io/cri-streaming serves them for CRI-O otherwise.
3//!
4//! Clients speak them over WebSocket, `v5.channel.k8s.io` and the versions
5//! before it, or over SPDY. The kubelet proxies them unchanged, but the API
6//! server translates `v5.channel.k8s.io` to SPDY, which kubectl arrives with
7//! then. Each transport turns the input of the client into [`ClientEvent`]s,
8//! and [`Output`] sends the output of a session the way its protocol needs
9//! it.
10
11use crate::{
12    container_io::Pipe,
13    streaming_server::{
14        idle::{Activity, STALL_TIMEOUT},
15        web::{self, Message, WebSocket},
16    },
17};
18use anyhow::{Context, Result, anyhow};
19use bytes::Bytes;
20use futures::{sink::SinkExt, stream::SplitSink};
21use std::{
22    future::{self, Future},
23    sync::{
24        Arc,
25        atomic::{AtomicBool, Ordering},
26    },
27};
28use tokio::time::{Duration, Instant};
29use tracing::debug;
30
31mod spdy;
32mod status;
33mod websocket;
34
35use spdy::SpdyOutput;
36pub(super) use spdy::{ExpectedStreams, SPDY_PROTOCOLS, SpdyStreams};
37pub(super) use status::{Operation, Status};
38pub(super) use websocket::{Framing, WebSocketInput};
39
40/// The channel of stdin, the first byte of a WebSocket message.
41pub(super) const STDIN_BYTE: u8 = 0;
42/// The channel of stdout.
43pub(super) const STDOUT_BYTE: u8 = 1;
44/// The channel of stderr.
45const STDERR_BYTE: u8 = 2;
46/// The channel of the status of the session.
47pub(super) const STREAM_ERR_BYTE: u8 = 3;
48/// The channel of the resize events.
49const RESIZE_BYTE: u8 = 4;
50/// The channel which closes another one, since `v5.channel.k8s.io`.
51pub(super) const CLOSE_BYTE: u8 = 255;
52
53// The channels of the pipes, see `Pipe::remotecommand_channel`.
54const _: () = assert!(
55    Pipe::StdOut.remotecommand_channel() == STDOUT_BYTE
56        && Pipe::StdErr.remotecommand_channel() == STDERR_BYTE
57);
58
59/// The status of a session which succeeded. The kubelet sends it as well,
60/// some clients take an empty error stream for a failure.
61const SUCCESS_STATUS: &[u8] = b"{\"metadata\":{},\"status\":\"Success\"}";
62
63/// A version of the protocols, which decides how a session reports its
64/// status and whether the client resizes terminals.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
66pub(super) enum Version {
67    V1,
68    V2,
69    V3,
70    V4,
71    V5,
72}
73
74impl Version {
75    /// Whether the status is JSON, otherwise only a failure is reported as
76    /// text.
77    fn json_status(self) -> bool {
78        self >= Self::V4
79    }
80
81    /// Whether a SPDY client with a terminal opens a stream for resize
82    /// events.
83    fn spdy_resize(self) -> bool {
84        self >= Self::V3
85    }
86}
87
88/// What a client sends during a session.
89#[derive(Debug, PartialEq)]
90pub(super) enum ClientEvent {
91    Stdin(Stdin),
92
93    /// A resize event as JSON.
94    Resize(Bytes),
95
96    /// The client closed the connection cleanly: a WebSocket with a close,
97    /// a SPDY connection which ended after the stdin of the client ended.
98    Closed,
99
100    /// The client sent a message larger than the server takes, which fails
101    /// the session, see [`MESSAGE_TOO_LARGE`].
102    TooLarge,
103}
104
105/// The failure of a session whose client sent a message larger than the
106/// server takes.
107pub(super) const MESSAGE_TOO_LARGE: &str =
108    "message of the client exceeds the size limit of the server";
109
110/// The stdin of a client.
111#[derive(Clone, Debug, PartialEq)]
112pub(super) enum Stdin {
113    Data(Bytes),
114
115    /// The client closed its stdin.
116    Close,
117}
118
119impl Stdin {
120    /// Stdin with data, none for an empty message, which carries nothing.
121    pub(super) fn data(data: Bytes) -> Option<Self> {
122        (!data.is_empty()).then_some(Self::Data(data))
123    }
124}
125
126/// The input of a client.
127pub(super) trait ClientInput: Send + 'static {
128    /// The next event, `None` once the client went away without a clean
129    /// close.
130    fn next(&mut self) -> impl Future<Output = Option<ClientEvent>> + Send;
131
132    /// Answer the clean close of the client, see [`web::answer_close`].
133    fn answer_close(&mut self) -> impl Future<Output = ()> + Send {
134        future::ready(())
135    }
136
137    /// End the connection once `output` closed it after
138    /// [`ClientEvent::TooLarge`], see [`web::drain`].
139    fn drain(self, output: Output) -> impl Future<Output = ()> + Send
140    where
141        Self: Sized,
142    {
143        drop(output);
144        future::ready(())
145    }
146}
147
148/// The sending half of a session. A failing send means that the client went
149/// away, which ends the session. After a clean close of a WebSocket client,
150/// sends fail as well, the output gets dropped then. The session still
151/// forwards the stdin which the client sent before its close to the process
152/// and reads the output meanwhile, so that the process does not wait for it.
153/// A SPDY connection which ends after the client closed its stdin counts as
154/// a clean close.
155pub(super) struct Output {
156    transport: Transport,
157    failed: bool,
158
159    /// Set once the client closed the connection cleanly.
160    peer_closed: Arc<AtomicBool>,
161
162    /// Whether a WebSocket gets closed for a message of the client which
163    /// was too large.
164    too_large: bool,
165
166    /// When the end of the session started, the status and the close share
167    /// the [`STALL_TIMEOUT`] from then on, a client which takes nothing does
168    /// not get it twice.
169    ending: Option<Instant>,
170
171    /// The idle timeout of the session, which its end waits for as well:
172    /// a client which takes nothing, like one which pauses a pager, keeps
173    /// the end until it took nothing for the idle timeout, at least for the
174    /// [`STALL_TIMEOUT`], see [`Activity::until_stalled_or_idle`]. Without
175    /// one, the end waits for the [`STALL_TIMEOUT`] only. A client which
176    /// vanished without a reset while its output backs up holds the end as
177    /// long as well, or until the kernel gives up resending to it, 15 to 30
178    /// minutes with the default `tcp_retries2` of 15: there is no keepalive,
179    /// and nothing waits for the echoes of pings. None once the session
180    /// failed for a message of the client which was too large, see
181    /// [`Self::close_too_large`].
182    idle_timeout: Option<Duration>,
183}
184
185enum Transport {
186    WebSocket {
187        sender: SplitSink<WebSocket, Message>,
188        framing: Framing,
189
190        /// Touched by every message with data which got sent, not by pings
191        /// and the close.
192        activity: Activity,
193
194        /// Touched whenever a part of what the server sends went out, see
195        /// [`web::ClientIo::written`].
196        written: Activity,
197    },
198    Spdy(SpdyOutput),
199}
200
201impl Output {
202    /// The output of a WebSocket session, whose end waits for the
203    /// `idle_timeout` of the session as well, see [`Self::idle_timeout`].
204    pub(super) fn websocket(
205        sender: SplitSink<WebSocket, Message>,
206        framing: Framing,
207        activity: Activity,
208        written: Activity,
209        idle_timeout: Option<Duration>,
210    ) -> Self {
211        let transport = Transport::WebSocket {
212            sender,
213            framing,
214            activity,
215            written,
216        };
217        Self::new(transport, Arc::default(), idle_timeout)
218    }
219
220    /// The output of a SPDY session, which shares whether the client closed
221    /// its stdin with its input, see [`SpdyInput`](spdy::SpdyInput), and
222    /// whose end waits for the `idle_timeout` of the session as well, see
223    /// [`Self::idle_timeout`].
224    pub(super) fn spdy(output: SpdyOutput, idle_timeout: Option<Duration>) -> Self {
225        let peer_closed = output.peer_closed.clone();
226        Self::new(Transport::Spdy(output), peer_closed, idle_timeout)
227    }
228
229    fn new(
230        transport: Transport,
231        peer_closed: Arc<AtomicBool>,
232        idle_timeout: Option<Duration>,
233    ) -> Self {
234        Self {
235            transport,
236            failed: false,
237            peer_closed,
238            too_large: false,
239            ending: None,
240            idle_timeout,
241        }
242    }
243
244    /// Close a WebSocket with the code for a message of the client which was
245    /// too large, which tells the client why the session failed. The session
246    /// failed, its status and its close do not wait for the idle timeout,
247    /// only for the [`STALL_TIMEOUT`].
248    pub(super) fn close_too_large(&mut self) {
249        self.too_large = true;
250        self.idle_timeout = None;
251    }
252
253    /// Set by the input once the client closed the connection cleanly.
254    pub(super) fn peer_closed(&self) -> Arc<AtomicBool> {
255        self.peer_closed.clone()
256    }
257
258    /// Tell a WebSocket client before v5 that the session started, with an
259    /// empty message on the first channel it gets output on.
260    pub(super) async fn announce(&mut self, stdout: bool, stderr: bool) -> Result<()> {
261        let Transport::WebSocket { framing, .. } = &self.transport else {
262            return Ok(());
263        };
264        if framing.version == Version::V5 {
265            return Ok(());
266        }
267        let channel = if stdout {
268            STDOUT_BYTE
269        } else if stderr {
270            STDERR_BYTE
271        } else {
272            STREAM_ERR_BYTE
273        };
274        let message = framing.message(channel, &[]);
275        self.send(message).await
276    }
277
278    /// Send output of the `pipe` of the process.
279    pub(super) async fn data(&mut self, pipe: Pipe, data: &Bytes) -> Result<()> {
280        let result = match &mut self.transport {
281            Transport::WebSocket { framing, .. } => {
282                let message = framing.message(pipe.remotecommand_channel(), data);
283                return self.send(message).await;
284            }
285            Transport::Spdy(output) => {
286                let stream = match pipe {
287                    Pipe::StdOut => &mut output.stdout,
288                    Pipe::StdErr => &mut output.stderr,
289                };
290                match stream {
291                    Some(stream) if !self.failed => stream.send(data.clone()).await,
292                    _ => Ok(()),
293                }
294            }
295        };
296        self.sent(result)
297    }
298
299    /// Send the final status of the session. Over SPDY it waits behind the
300    /// output queued before, a slow client gets it all the same, one which
301    /// stopped reading for [`STALL_TIMEOUT`] does not get it, see
302    /// [`Self::idle_timeout`].
303    pub(super) async fn status(&mut self, status: &Status) -> Result<()> {
304        let ending = self.ending();
305        let idle_timeout = self.idle_timeout;
306        self.written()
307            .until_stalled_or_idle(
308                ending,
309                STALL_TIMEOUT,
310                idle_timeout,
311                self.send_status(status),
312            )
313            .await
314            .unwrap_or_else(|| Err(anyhow!("client stopped reading before the status")))
315    }
316
317    /// When the end of the session started, the first time this got
318    /// called.
319    fn ending(&mut self) -> Instant {
320        *self.ending.get_or_insert_with(Instant::now)
321    }
322
323    /// Touched whenever a part of what the server sends went out.
324    fn written(&self) -> Activity {
325        match &self.transport {
326            Transport::WebSocket { written, .. } => written.clone(),
327            Transport::Spdy(output) => output.connection.written(),
328        }
329    }
330
331    async fn send_status(&mut self, status: &Status) -> Result<()> {
332        let result = match &mut self.transport {
333            Transport::WebSocket { framing, .. } => {
334                let Some(status) = status.encode(framing.version)? else {
335                    return Ok(());
336                };
337                let message = framing.message(STREAM_ERR_BYTE, &status);
338                return self.send(message).await;
339            }
340            Transport::Spdy(output) => match status.encode(output.version)? {
341                Some(status) if !self.failed => output.error.send(status.into()).await,
342                _ => Ok(()),
343            },
344        };
345        self.sent(result)
346    }
347
348    /// Check that the client is still there while the session waits. A ping
349    /// to a client which went away fails. A SPDY connection pings the client
350    /// itself, also when it is not read while stdin waits for the process,
351    /// and ends once a write fails.
352    pub(super) async fn ping(&mut self) -> Result<()> {
353        match &self.transport {
354            Transport::WebSocket { .. } => self.send(Message::Ping(Bytes::new())).await,
355            Transport::Spdy(output) if output.connection.done().is_cancelled() => {
356                self.sent(Err(anyhow!("SPDY connection closed")))
357            }
358            Transport::Spdy(_) => Ok(()),
359        }
360    }
361
362    /// End the session: a WebSocket gets closed, the SPDY streams end and the
363    /// writer flushes what is queued, shuts the connection down and ends it.
364    /// A client which takes it slowly gets all of it, one which stopped
365    /// reading gets the connection ended after [`STALL_TIMEOUT`], counted
366    /// from the status on, see [`Self::idle_timeout`]. Whether the close
367    /// went out, which the client of a WebSocket answers.
368    pub(super) async fn close(&mut self) -> bool {
369        let ending = self.ending();
370        let written = self.written();
371        let idle_timeout = self.idle_timeout;
372        let close = async {
373            match &mut self.transport {
374                Transport::WebSocket { .. } => {
375                    if let Err(e) = self.send(web::close(self.too_large, "done")).await {
376                        debug!("Unable to send close message: {e:#}");
377                        return false;
378                    }
379                    // The client closed first, its close gets the answer
380                    // instead, if its read did not write it yet.
381                    if self.closed()
382                        && let Transport::WebSocket { sender, .. } = &mut self.transport
383                    {
384                        web::write_close_answer(sender).await;
385                    }
386                }
387                Transport::Spdy(output) => {
388                    for stream in [
389                        output.stdin.as_mut(),
390                        output.stdout.as_mut(),
391                        output.stderr.as_mut(),
392                    ]
393                    .into_iter()
394                    .flatten()
395                    .chain([&mut output.error])
396                    {
397                        if let Err(e) = stream.close().await {
398                            debug!("Unable to close stream: {e:#}");
399                        }
400                    }
401                    output.connection.close();
402                    output.connection.done().cancelled().await;
403                }
404            }
405            true
406        };
407        let closed = written
408            .until_stalled_or_idle(ending, STALL_TIMEOUT, idle_timeout, close)
409            .await
410            .unwrap_or_else(|| {
411                debug!("Client stopped reading before the end of the session");
412                false
413            });
414        if let Transport::Spdy(output) = &self.transport {
415            output.connection.abort();
416        }
417        closed
418    }
419
420    /// Send a WebSocket message. A failure ends the session, unless the
421    /// client closed the connection cleanly, see [`Output`].
422    async fn send(&mut self, message: Message) -> Result<()> {
423        if self.failed {
424            return Ok(());
425        }
426        let Transport::WebSocket {
427            sender, activity, ..
428        } = &mut self.transport
429        else {
430            return Ok(());
431        };
432        let data = !matches!(message, Message::Ping(_) | Message::Close(_));
433        let result = sender.send(message).await;
434        // The input did not tell about the close yet.
435        if result.as_ref().is_err_and(web::is_closed_by_client) {
436            self.peer_closed.store(true, Ordering::Release);
437        }
438        let result = result.map_err(anyhow::Error::from);
439        if result.is_ok() && data {
440            activity.touch();
441        }
442        self.sent(result)
443    }
444
445    /// A send failed after a clean close: of a WebSocket, or of a SPDY
446    /// connection which ended after the stdin of the client ended. A stream
447    /// of a connection which is still up got reset, which fails the session.
448    fn closed(&self) -> bool {
449        self.peer_closed.load(Ordering::Acquire)
450            && match &self.transport {
451                Transport::WebSocket { .. } => true,
452                Transport::Spdy(output) => output.connection.done().is_cancelled(),
453            }
454    }
455
456    fn sent(&mut self, result: Result<()>) -> Result<()> {
457        match result {
458            Ok(()) => Ok(()),
459            Err(e) if self.closed() => {
460                debug!("Dropping the output of the session after the close: {e:#}");
461                self.failed = true;
462                Ok(())
463            }
464            Err(e) => Err(e).context("send to client"),
465        }
466    }
467}
468
469/// The entry points of the fuzz targets.
470#[cfg(any(test, feature = "fuzzing"))]
471pub(super) mod fuzzing;
472
473#[cfg(test)]
474mod tests;