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;