conmonrs/streaming_server/port_forward.rs
1//! Port forwarding into the network namespace of a pod, the Kubernetes
2//! `portforward.k8s.io` protocol, as k8s.io/cri-streaming serves it for
3//! CRI-O otherwise.
4//!
5//! Over SPDY, the client opens two streams per forwarded connection, both
6//! with the `port` and `requestID` headers: an `error` stream, on which the
7//! server reports why forwarding failed, and a `data` stream, which carries
8//! the connection. The kubelet always uses SPDY, direct clients like crictl
9//! may tunnel it through a WebSocket instead.
10//!
11//! A request waits for its second stream without reading the first one.
12//! client-go opens the error stream first and waits for its reply, a client
13//! which sends a lot on a data stream before opening its error stream holds
14//! up the connection until the request times out.
15//!
16//! The older WebSocket protocol forwards one connection per port in the
17//! query of the request. Each port gets a data and an error channel, every
18//! message starts with its channel and the first one of each channel carries
19//! the port.
20
21use crate::streaming_server::{
22 budget::{Budget, Held, HeldBuf},
23 spdy::{PING_INTERVAL, StreamReceiver},
24};
25use anyhow::{Context, Result, bail, ensure};
26use bytes::{BufMut, Bytes, BytesMut};
27use std::{
28 future::{self, Future},
29 io,
30 path::PathBuf,
31 pin::pin,
32};
33use tokio::{
34 io::{AsyncReadExt, AsyncWriteExt},
35 net::{
36 TcpSocket, TcpStream,
37 tcp::{OwnedReadHalf, OwnedWriteHalf},
38 },
39 select,
40 sync::mpsc,
41 time::{self, Duration},
42};
43use tracing::debug;
44
45mod channels;
46mod namespace;
47mod requests;
48mod tunnel;
49
50use namespace::Namespace;
51pub(super) use tunnel::WebSocketProtocol;
52
53/// The protocol the client has to ask for in the upgrade request.
54pub(super) const PROTOCOL: &str = "portforward.k8s.io";
55
56const PORT_HEADER: &str = "port";
57const REQUEST_ID_HEADER: &str = "requestID";
58const STREAM_TYPE_DATA: &str = "data";
59const STREAM_TYPE_ERROR: &str = "error";
60
61/// How long connecting to a port in the pod may take. Within the pod it gets
62/// accepted or refused right away, unless a firewall drops it.
63const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
64
65/// The stack of the thread which creates the sockets of a session within its
66/// network namespace.
67const SOCKET_THREAD_STACK_SIZE: usize = 128 * 1024;
68
69/// How long the thread waits for the next connection before it ends, which
70/// leaves the network namespace.
71const SOCKET_THREAD_LINGER: Duration = Duration::from_secs(1);
72
73/// How much a client may send for a connection before it got established in
74/// the pod, a client which sends more fails the forwarding.
75const CONNECT_BUFFER: usize = 1024 * 1024;
76
77/// How much the connections of a session which get established hold for the
78/// client together. Their data moves from the byte budget of the client into
79/// this one, so that connections to a port which does not answer, which the
80/// connect timeout could take long, do not stop the others. Once it is used
81/// up, connecting streams stop taking their data, which then stays in the
82/// byte budget of the client and may stop reading the client like any
83/// stream which does not take its data. Any number of connections get
84/// established at once, like with CRI-O.
85const CONNECT_BUDGET: u32 = 16 * 1024 * 1024;
86
87/// How much of the container output goes into a single frame, as much as Go
88/// clients send per frame.
89const READ_BUFFER: usize = 32 * 1024;
90
91/// How long the client gets to close its side of a connection after the
92/// container closed its side, and the container to send the rest of its
93/// output after writing to it failed, like CRI-O waits.
94const CLOSE_GRACE_PERIOD: Duration = Duration::from_secs(1);
95
96/// How long the pod gets to answer and close its side of a SPDY connection
97/// after the client closed its side. A client whose local application went
98/// away looks the same as one which only finished its request: client-go
99/// closes the data stream first, and resetting a stream it closed already
100/// sends nothing. Without a bound, a pod which never stops sending would
101/// keep the streams of such a request, which count against the most streams
102/// of the connection, for as long as the connection lasts. CRI-O ends both
103/// directions [`CLOSE_GRACE_PERIOD`] after either one finished, this is long
104/// enough for the answer to a request which ends with the client closing
105/// its side.
106const HALF_CLOSE_GRACE_PERIOD: Duration = Duration::from_secs(30);
107
108/// The WebSocket subprotocol of SPDY tunneled through a WebSocket.
109const SPDY_TUNNEL_PROTOCOL: &str = "SPDY/3.1+portforward.k8s.io";
110
111/// The WebSocket subprotocols of the channel protocol, which is binary
112/// without a subprotocol as well.
113const CHANNEL_PROTOCOL: &str = "v4.channel.k8s.io";
114const BASE64_CHANNEL_PROTOCOL: &str = "v4.base64.channel.k8s.io";
115
116/// The query parameter with the ports of the channel protocol.
117const PORT_QUERY: &str = "port";
118
119/// The most ports of a channel protocol request. Base64 clients get the
120/// channel as an ASCII digit offset, which stays a single byte for the
121/// channels of these ports.
122const MAX_CHANNEL_PORTS: usize = 40;
123
124/// How many messages of the client wait for a forwarded connection, before
125/// the WebSocket is not read anymore.
126const CHANNEL_QUEUE: usize = 16;
127
128/// How many bytes of the client the ports of a WebSocket hold together,
129/// before the WebSocket is not read anymore. Without it, a client could make
130/// the server hold [`CHANNEL_QUEUE`] messages of up to the largest message
131/// size for each of [`MAX_CHANNEL_PORTS`] ports which do not take them.
132const CHANNEL_BUFFER: u32 = 4 * 1024 * 1024;
133
134/// How many messages for the client wait for the WebSocket.
135const OUTPUT_QUEUE: usize = 16;
136
137/// The size of the buffers between the WebSocket and the tunneled SPDY
138/// connection.
139const TUNNEL_BUFFER: usize = 64 * 1024;
140
141/// The data a client sends for a forwarded connection. It counts against
142/// the budget of the client until it got written to the pod.
143trait Input {
144 /// The next data, `None` once the client is done.
145 async fn recv(&mut self) -> Option<Held>;
146}
147
148impl Input for StreamReceiver {
149 async fn recv(&mut self) -> Option<Held> {
150 self.recv_held().await
151 }
152}
153
154impl Input for mpsc::Receiver<Held> {
155 async fn recv(&mut self) -> Option<Held> {
156 mpsc::Receiver::recv(self).await
157 }
158}
159
160/// What a client sent while its connection got established, within the
161/// connect budget of the session, see [`CONNECT_BUDGET`]. It gets collected
162/// into one buffer, small data does not take an allocation each.
163#[derive(Debug, Default)]
164struct Early {
165 data: HeldBuf,
166
167 /// What the client sent last, which did not get into the connect budget
168 /// before the connection got established.
169 last: Option<Held>,
170
171 /// Whether the client was done already.
172 ended: bool,
173}
174
175/// A port forward session of a pod.
176#[derive(Debug)]
177pub(super) struct PortForward {
178 netns_path: PathBuf,
179 idle_timeout: Option<Duration>,
180 ping_interval: Option<Duration>,
181}
182
183impl PortForward {
184 /// A session for the network namespace at `netns_path`, which ends once it
185 /// was idle for `idle_timeout`.
186 pub(super) fn new(netns_path: PathBuf, idle_timeout: Option<Duration>) -> Self {
187 Self {
188 netns_path,
189 idle_timeout,
190 ping_interval: Some(PING_INTERVAL),
191 }
192 }
193
194 /// Serve SPDY without pinging the client.
195 #[cfg(test)]
196 fn without_pings(mut self) -> Self {
197 self.ping_interval = None;
198 self
199 }
200
201 /// Connect to the port in the pod while taking what the client sends
202 /// meanwhile.
203 async fn establish(
204 namespace: &Namespace,
205 port: u16,
206 input: &mut impl Input,
207 ) -> Result<(TcpStream, Early)> {
208 Self::drain_until(
209 Self::connect(namespace, port),
210 &namespace.connect_budget,
211 input,
212 )
213 .await
214 }
215
216 /// Wait for the connection to the pod while taking what the client sends
217 /// meanwhile, which may take until the connect timeout. The data moves
218 /// from the byte budget of the client into the connect budget, so that
219 /// the client can go on with its other connections. Without room in the
220 /// connect budget no more data gets taken, see [`CONNECT_BUDGET`]. A
221 /// client which sends more than [`CONNECT_BUFFER`] fails the forwarding.
222 async fn drain_until<T>(
223 connect: impl Future<Output = Result<T>>,
224 connect_budget: &Budget,
225 input: &mut impl Input,
226 ) -> Result<(T, Early)> {
227 let mut connect = pin!(connect);
228 let mut early = Early::default();
229 let mut len = 0;
230 // Data which got taken and waits for room in the connect budget.
231 let mut taken: Option<Held> = None;
232 loop {
233 select! {
234 result = &mut connect => {
235 early.last = taken;
236 return Ok((result?, early));
237 }
238 held = async {
239 match &taken {
240 Some(data) => {
241 let cost = early.data.cost(data.len());
242 connect_budget.hold_as(Bytes::clone(data), cost).await
243 }
244 None => future::pending().await,
245 }
246 } => {
247 // The data leaves the byte budget of the client.
248 taken = None;
249 early.data.push(held);
250 }
251 data = input.recv(), if taken.is_none() && !early.ended => match data {
252 Some(data) => {
253 len += data.len();
254 ensure!(
255 len <= CONNECT_BUFFER,
256 "the client sent more than {CONNECT_BUFFER} bytes before the connection got established"
257 );
258 taken = Some(data);
259 }
260 None => early.ended = true,
261 },
262 }
263 }
264 }
265
266 /// The next output of the pod, at most [`READ_BUFFER`] bytes, none once it
267 /// closed its side. Nothing gets lost if it got cancelled.
268 async fn read_pod(
269 from_container: &mut OwnedReadHalf,
270 buf: &mut BytesMut,
271 ) -> Result<Option<Bytes>> {
272 buf.reserve(READ_BUFFER);
273 let read = from_container
274 .read_buf(&mut (&mut *buf).limit(READ_BUFFER))
275 .await
276 .context("read from the pod")?;
277 Ok((read > 0).then(|| buf.split().freeze()))
278 }
279
280 /// What the client gets told about a failed connection of a port.
281 fn forward_error(port: u16, e: &anyhow::Error) -> String {
282 format!("error forwarding port {port}: {e:#}")
283 }
284
285 /// Write what the client sends to the pod until the client is done,
286 /// starting with what it sent while the connection got established,
287 /// which gives its budget back once it got written.
288 async fn write_input(
289 early: Early,
290 input: &mut impl Input,
291 to_container: &mut OwnedWriteHalf,
292 ) -> Result<()> {
293 let Early { data, last, ended } = early;
294 to_container
295 .write_all(&data)
296 .await
297 .context("write to the pod")?;
298 drop(data);
299 if let Some(last) = last {
300 to_container
301 .write_all(&last)
302 .await
303 .context("write to the pod")?;
304 }
305 if !ended {
306 while let Some(data) = input.recv().await {
307 to_container
308 .write_all(&data)
309 .await
310 .context("write to the pod")?;
311 }
312 }
313 Ok(())
314 }
315
316 /// End a connection whose writes to the pod failed with `e`. The pod may
317 /// have answered before it closed the connection, like a server which
318 /// rejects an upload early, so the `downstream` copy gets up to
319 /// [`CLOSE_GRACE_PERIOD`] to forward that, like CRI-O waits for the other
320 /// direction.
321 async fn upstream_failed(
322 e: anyhow::Error,
323 downstream: impl Future<Output = Result<()>>,
324 ) -> Result<()> {
325 if time::timeout(CLOSE_GRACE_PERIOD, downstream).await.is_err() {
326 debug!("The pod did not close its side of the connection");
327 }
328 Err(e)
329 }
330
331 /// Connect to the port in the network namespace of the pod. The pod may
332 /// listen on either family, they get tried one after the other like CRI-O
333 /// does. Nagle's algorithm is off like in Go, so that request frames
334 /// which get written one by one do not wait for a delayed ACK.
335 async fn connect(namespace: &Namespace, port: u16) -> Result<TcpStream> {
336 let mut errors = vec![];
337 for (ip, socket) in namespace.sockets().await? {
338 let result = match socket {
339 Ok(socket) => {
340 let socket = TcpSocket::from_std_stream(socket);
341 if let Err(e) = socket.set_nodelay(true) {
342 debug!("Unable to disable Nagle's algorithm: {e}");
343 }
344 time::timeout(CONNECT_TIMEOUT, socket.connect((ip, port).into()))
345 .await
346 .unwrap_or_else(|_| Err(io::ErrorKind::TimedOut.into()))
347 }
348 Err(e) => Err(e),
349 };
350 match result {
351 Ok(connection) => return Ok(connection),
352 Err(e) => errors.push(format!("{ip}: {e}")),
353 }
354 }
355 bail!(
356 "failed to connect to localhost:{port} inside network namespace {}: {}",
357 namespace.path.display(),
358 errors.join(", ")
359 )
360 }
361}
362
363/// The entry points of the fuzz targets.
364#[cfg(any(test, feature = "fuzzing"))]
365pub(super) mod fuzzing;
366
367#[cfg(test)]
368mod tests;