Skip to main content

conmonrs/
server.rs

1#![deny(missing_docs)]
2
3#[cfg(feature = "tracing")]
4use crate::telemetry::Telemetry;
5use crate::{
6    child_reaper::ChildReaper,
7    config::{Commands, Config, LogDriver, LogLevel, Verbosity},
8    container_io::{ContainerIO, ContainerIOType},
9    fd_socket::FdSocket,
10    init::{DefaultInit, Init},
11    journal::Journal,
12    listener::{DefaultListener, Listener},
13    pause::Pause,
14    streaming_server::StreamingServer,
15    version::Version,
16};
17use anyhow::{Context, Result, format_err};
18use capnp::text_list::Reader;
19use capnp_rpc::{RpcSystem, rpc_twoparty_capnp::Side, twoparty};
20#[cfg(feature = "tracing")]
21use clap::crate_name;
22use conmon_common::conmon_capnp::conmon::{self, CgroupManager};
23use futures::{AsyncReadExt, FutureExt};
24use libc::_exit;
25use nix::{
26    errno::Errno,
27    sys::signal::Signal,
28    unistd::{ForkResult, fork},
29};
30#[cfg(feature = "tracing")]
31use opentelemetry::trace::{FutureExt as OpenTelemetryFutureExt, TracerProvider};
32#[cfg(feature = "tracing")]
33use opentelemetry_sdk::trace::SdkTracerProvider;
34use std::{fs::File, io::Write, os::fd::RawFd, path::Path, process, str::FromStr, sync::Arc};
35use tokio::{
36    fs,
37    runtime::{Builder, Handle},
38    signal::unix::{SignalKind, signal},
39    sync::{RwLock, oneshot},
40    task::{self, LocalSet},
41};
42use tokio_util::compat::TokioAsyncReadCompatExt;
43use tracing::{Instrument, debug, debug_span, info};
44#[cfg(feature = "tracing")]
45use tracing_opentelemetry::OpenTelemetrySpanExt;
46use tracing_subscriber::{filter::LevelFilter, layer::SubscriberExt, prelude::*};
47use twoparty::VatNetwork;
48
49#[derive(Debug)]
50/// The main server structure.
51pub struct Server {
52    /// Server configuration.
53    config: Arc<Config>,
54
55    /// Child reaper instance.
56    reaper: Arc<ChildReaper>,
57
58    /// Fd socket instance.
59    fd_socket: Arc<FdSocket>,
60
61    /// OpenTelemetry tracer instance.
62    #[cfg(feature = "tracing")]
63    tracer: Option<SdkTracerProvider>,
64
65    /// Streaming server instance.
66    streaming_server: Arc<RwLock<StreamingServer>>,
67}
68
69impl Server {
70    /// Server configuration.
71    pub(crate) fn config(&self) -> &Arc<Config> {
72        &self.config
73    }
74
75    /// Child reaper instance.
76    pub(crate) fn reaper(&self) -> &Arc<ChildReaper> {
77        &self.reaper
78    }
79
80    /// Fd socket instance.
81    pub(crate) fn fd_socket(&self) -> &Arc<FdSocket> {
82        &self.fd_socket
83    }
84
85    /// OpenTelemetry tracer instance.
86    #[cfg(feature = "tracing")]
87    pub(crate) fn tracer(&self) -> &Option<SdkTracerProvider> {
88        &self.tracer
89    }
90
91    /// Streaming server instance.
92    pub(crate) fn streaming_server(&self) -> &Arc<RwLock<StreamingServer>> {
93        &self.streaming_server
94    }
95
96    /// Create a new `Server` instance.
97    pub fn new() -> Result<Self> {
98        let server = Self {
99            config: Arc::new(Config::default()),
100            reaper: Default::default(),
101            fd_socket: Default::default(),
102            #[cfg(feature = "tracing")]
103            tracer: Default::default(),
104            streaming_server: Default::default(),
105        };
106
107        if let Some(v) = server.config().version() {
108            Version::new(v == Verbosity::Full).print();
109            process::exit(0);
110        }
111
112        if let Some(v) = server.config().version_json() {
113            Version::new(v == Verbosity::Full).print_json()?;
114            process::exit(0);
115        }
116
117        if let Some(Commands::Pause {
118            base_path,
119            pod_id,
120            ipc,
121            pid,
122            net,
123            user,
124            uts,
125            uid_mappings,
126            gid_mappings,
127        }) = server.config().command()
128        {
129            Pause::run(
130                base_path,
131                pod_id,
132                *ipc,
133                *pid,
134                *net,
135                *user,
136                *uts,
137                uid_mappings,
138                gid_mappings,
139            )
140            .context("run pause")?;
141            process::exit(0);
142        }
143
144        server.config().validate().context("validate config")?;
145
146        Self::init().context("init self")?;
147        Ok(server)
148    }
149
150    /// Close file descriptors inherited from whatever started us.
151    ///
152    /// conmon-rs daemonizes and outlives the process that spawned it, so any
153    /// descriptor it inherits stays open for the lifetime of the server. If
154    /// the spawning process was itself started with extra descriptors open,
155    /// holding them can keep that process tree from ever finishing - a test
156    /// harness waiting for its own pipe to close, for example.
157    ///
158    /// conmon does the same thing, see `close_all_fds_ge_than(3)` there. Any
159    /// descriptor conmon-rs actually needs is either created after this point
160    /// or received later over the fd socket, so nothing above stderr is worth
161    /// keeping.
162    fn close_inherited_fds() {
163        for fd in Self::inherited_fds() {
164            // The descriptor used to enumerate them is already closed by now,
165            // so check before closing to avoid closing an unrelated descriptor
166            // that reused the number. This runs before tokio starts, so nothing
167            // else can be opening descriptors concurrently.
168            //
169            // These are raw libc calls rather than the `nix` wrappers on
170            // purpose: `BorrowedFd`/`OwnedFd` require the descriptor to be open
171            // for the duration of the borrow, and whether it is still open is
172            // exactly what is unknown here. `fcntl(2)` and `close(2)` take a
173            // plain integer with no such precondition and report `EBADF`.
174            //
175            // SAFETY: both calls are defined for any integer; a stale or
176            // invalid descriptor number fails with `EBADF` rather than
177            // affecting anything else.
178            if unsafe { libc::fcntl(fd, libc::F_GETFD) } != -1 {
179                unsafe { libc::close(fd) };
180            }
181        }
182    }
183
184    /// Every open descriptor above stderr. Split out from `close_inherited_fds`
185    /// so it can be tested without closing the test runner's own descriptors.
186    ///
187    /// This is Linux only: it enumerates `/proc/self/fd` and would report
188    /// nothing at all where that is unavailable, silently leaking the inherited
189    /// descriptors. conmon-rs already requires Linux elsewhere (`prctl`, epoll,
190    /// journald), so the assertion below just makes a port fail here loudly
191    /// rather than lose the behaviour quietly. A log line would not work,
192    /// `close_inherited_fds` runs well before `init_logging`.
193    fn inherited_fds() -> Vec<RawFd> {
194        const {
195            assert!(
196                cfg!(target_os = "linux"),
197                "inherited fds are enumerated through /proc/self/fd, which is Linux only"
198            )
199        };
200
201        let mut fds = Vec::new();
202
203        if let Ok(entries) = std::fs::read_dir("/proc/self/fd") {
204            for entry in entries.flatten() {
205                if let Some(fd) = entry
206                    .file_name()
207                    .to_str()
208                    .and_then(|name| name.parse::<RawFd>().ok())
209                    && fd > 2
210                {
211                    fds.push(fd);
212                }
213            }
214        }
215
216        fds
217    }
218
219    /// Start the `Server` instance and consume it.
220    pub fn start(self) -> Result<()> {
221        // We need to fork as early as possible, especially before setting up tokio.
222        // If we don't, the child will have a strange thread space and we're at risk of deadlocking.
223        // We also have to treat the parent as the child (as described in [1]) to ensure we don't
224        // interrupt the child's execution.
225        // 1: https://docs.rs/nix/0.23.0/nix/unistd/fn.fork.html#safety
226        if !self.config().skip_fork() {
227            match unsafe { fork()? } {
228                ForkResult::Parent { child, .. } => {
229                    write!(File::create(self.config().conmon_pidfile())?, "{child}")?;
230                    unsafe { _exit(0) };
231                }
232                ForkResult::Child => (),
233            }
234        }
235
236        // Deliberately outside the `skip_fork` check above: the server outlives
237        // its parent either way, so the descriptors have to go either way.
238        Self::close_inherited_fds();
239
240        // now that we've forked, set self to childreaper
241        let ret = unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) };
242        if ret != 0 {
243            return Err(Errno::last()).context("set child subreaper");
244        }
245
246        #[cfg(feature = "tracing")]
247        let tracer = self.tracer().clone();
248
249        debug!("Configuring Tokio runtime with current_thread");
250        let rt = Builder::new_current_thread()
251            .enable_io()
252            .enable_time()
253            .build()?;
254        rt.block_on(self.spawn_tasks())?;
255
256        #[cfg(feature = "tracing")]
257        if let Some(tracer) = tracer {
258            tracer.shutdown().context("shutdown tracer")?;
259        }
260
261        rt.shutdown_timeout(std::time::Duration::from_secs(15));
262        Ok(())
263    }
264
265    fn init() -> Result<()> {
266        let init = Init::<DefaultInit>::default();
267        init.unset_locale()?;
268        init.set_default_umask();
269        // While we could configure this, standard practice has it as -1000,
270        // so it may be YAGNI to add configuration.
271        init.set_oom_score("-1000")
272    }
273
274    fn init_logging(&mut self) -> Result<()> {
275        let level = LevelFilter::from_str(self.config().log_level().as_ref())
276            .context("convert log level filter")?;
277
278        #[cfg(feature = "tracing")]
279        let telemetry_layer = if self.config().enable_tracing() {
280            let tracer = Telemetry::layer(self.config().tracing_endpoint())
281                .context("build telemetry layer")?;
282
283            self.tracer = Some(tracer.clone());
284
285            tracing_opentelemetry::layer()
286                .with_tracer(tracer.tracer(crate_name!()))
287                .into()
288        } else {
289            None
290        };
291
292        let registry = tracing_subscriber::registry();
293        #[cfg(feature = "tracing")]
294        let registry = registry.with(telemetry_layer);
295
296        match self.config().log_driver() {
297            LogDriver::None => {}
298            LogDriver::Stdout => {
299                let layer = tracing_subscriber::fmt::layer()
300                    .with_target(true)
301                    .with_line_number(true)
302                    .with_filter(level);
303                registry
304                    .with(layer)
305                    .try_init()
306                    .context("init stdout fmt layer")?;
307                info!("Using stdout logger");
308            }
309            LogDriver::Systemd => {
310                let layer = tracing_subscriber::fmt::layer()
311                    .with_target(true)
312                    .with_line_number(true)
313                    .without_time()
314                    .with_writer(Journal)
315                    .with_filter(level);
316                registry
317                    .with(layer)
318                    .try_init()
319                    .context("init journald fmt layer")?;
320                info!("Using systemd/journald logger");
321            }
322        }
323        info!("Set log level to: {}", self.config().log_level());
324        Ok(())
325    }
326
327    /// Spawns all required tokio tasks.
328    async fn spawn_tasks(mut self) -> Result<()> {
329        self.init_logging().context("init logging")?;
330
331        let (shutdown_tx, shutdown_rx) = oneshot::channel();
332        let socket = self.config().socket();
333        let fd_socket = self.config().fd_socket();
334        let reaper = self.reaper.clone();
335
336        let signal_handler_span = debug_span!("signal_handler");
337        let backend_span = debug_span!("backend");
338
339        // Run both signal handler and backend inside spawn_blocking with LocalSet
340        // This allows concurrent execution within the LocalSet while preventing
341        // blocking of the main current_thread runtime
342        #[cfg(feature = "tracing")]
343        let result = task::spawn_blocking(move || {
344            Handle::current().block_on(async move {
345                let local = LocalSet::new();
346
347                // Spawn signal handler as a local task
348                local.spawn_local(
349                    Self::start_signal_handler(reaper, socket, fd_socket, shutdown_tx)
350                        .with_context(signal_handler_span.context())
351                        .instrument(signal_handler_span),
352                );
353
354                // Run backend on the LocalSet
355                local
356                    .run_until(
357                        self.start_backend(shutdown_rx)
358                            .with_context(backend_span.context())
359                            .instrument(backend_span),
360                    )
361                    .await
362            })
363        })
364        .await?;
365        #[cfg(not(feature = "tracing"))]
366        let result = task::spawn_blocking(move || {
367            Handle::current().block_on(async move {
368                let local = LocalSet::new();
369
370                // Spawn signal handler as a local task
371                local.spawn_local(
372                    Self::start_signal_handler(reaper, socket, fd_socket, shutdown_tx)
373                        .instrument(signal_handler_span),
374                );
375
376                // Run backend on the LocalSet
377                local
378                    .run_until(self.start_backend(shutdown_rx).instrument(backend_span))
379                    .await
380            })
381        })
382        .await?;
383        result
384    }
385
386    async fn start_signal_handler<T: AsRef<Path>>(
387        reaper: Arc<ChildReaper>,
388        socket: T,
389        fd_socket: T,
390        shutdown_tx: oneshot::Sender<()>,
391    ) -> Result<()> {
392        let mut sigterm = signal(SignalKind::terminate())?;
393        let mut sigint = signal(SignalKind::interrupt())?;
394
395        tokio::select! {
396            _ = sigterm.recv() => {
397                info!("Received SIGTERM");
398            }
399            _ = sigint.recv() => {
400                info!("Received SIGINT");
401            }
402        }
403
404        if let Some(pause) = Pause::maybe_shared() {
405            pause.stop();
406        }
407
408        debug!("Starting grandchildren cleanup task");
409        // Always use SIGKILL to ensure immediate termination of container processes
410        reaper
411            .kill_grandchildren(Signal::SIGKILL)
412            .await
413            .context("unable to kill grandchildren")?;
414
415        debug!("Sending shutdown message");
416        shutdown_tx
417            .send(())
418            .map_err(|_| format_err!("unable to send shutdown message"))?;
419
420        debug!("Removing socket file {}", socket.as_ref().display());
421        fs::remove_file(socket)
422            .await
423            .context("remove existing socket file")?;
424
425        debug!("Removing fd socket file {}", fd_socket.as_ref().display());
426        fs::remove_file(fd_socket)
427            .await
428            .or_else(|err| {
429                if err.kind() == std::io::ErrorKind::NotFound {
430                    Ok(())
431                } else {
432                    Err(err)
433                }
434            })
435            .context("remove existing fd socket file")
436    }
437
438    async fn start_backend(self, mut shutdown_rx: oneshot::Receiver<()>) -> Result<()> {
439        let listener =
440            Listener::<DefaultListener>::default().bind_long_path(self.config().socket())?;
441        let client: conmon::Client = capnp_rpc::new_client(self);
442
443        loop {
444            let stream = tokio::select! {
445                _ = &mut shutdown_rx => {
446                    debug!("Received shutdown message");
447                    return Ok(())
448                }
449                stream = listener.accept() => {
450                    stream?.0
451                },
452            };
453            let (reader, writer) = TokioAsyncReadCompatExt::compat(stream).split();
454            let network = Box::new(VatNetwork::new(
455                reader,
456                writer,
457                Side::Server,
458                Default::default(),
459            ));
460            let rpc_system = RpcSystem::new(network, Some(client.clone().client));
461            task::spawn_local(Box::pin(rpc_system.map(|_| ())));
462        }
463    }
464}
465
466pub(crate) struct GenerateRuntimeArgs<'a> {
467    pub(crate) config: &'a Config,
468    pub(crate) id: &'a str,
469    pub(crate) container_io: &'a ContainerIO,
470    pub(crate) pidfile: &'a Path,
471    pub(crate) cgroup_manager: CgroupManager,
472}
473
474impl GenerateRuntimeArgs<'_> {
475    const SYSTEMD_CGROUP_ARG: &'static str = "--systemd-cgroup";
476    const RUNTIME_CRUN: &'static str = "crun";
477    const LOG_LEVEL_FLAG_CRUN: &'static str = "--log-level";
478
479    /// Generate the OCI runtime CLI arguments from the provided parameters.
480    pub fn create_args(
481        self,
482        bundle_path: &Path,
483        global_args: Reader,
484        command_args: Reader,
485    ) -> Result<Vec<String>> {
486        // Pre-allocate capacity for typical arg count to reduce reallocations
487        let mut args = Vec::with_capacity(16);
488        args.extend(self.default_args().context("build default runtime args")?);
489
490        if let Some(rr) = self.config.runtime_root() {
491            args.push(format!("--root={}", rr.display()));
492        }
493
494        if self.cgroup_manager == CgroupManager::Systemd {
495            args.push(Self::SYSTEMD_CGROUP_ARG.into());
496        }
497
498        for arg in global_args {
499            args.push(arg?.to_string()?);
500        }
501
502        // Use static strings where possible to avoid allocations
503        args.push("create".into());
504        args.push("--bundle".into());
505        args.push(bundle_path.display().to_string());
506        args.push("--pid-file".into());
507        args.push(self.pidfile.display().to_string());
508
509        for arg in command_args {
510            args.push(arg?.to_string()?);
511        }
512
513        if let ContainerIOType::Terminal(terminal) = self.container_io.typ() {
514            args.push(format!("--console-socket={}", terminal.path().display()));
515        }
516
517        args.push(self.id.into());
518
519        debug!("Runtime args {:?}", args.join(" "));
520        Ok(args)
521    }
522
523    /// Generate the OCI runtime CLI arguments from the provided parameters.
524    pub(crate) fn exec_sync_args(&self, command: Reader) -> Result<Vec<String>> {
525        let mut args = self
526            .exec_sync_args_without_command()
527            .context("exec sync args without command")?;
528
529        for arg in command {
530            args.push(arg?.to_string()?);
531        }
532
533        debug!("Exec args {:?}", args.join(" "));
534        Ok(args)
535    }
536
537    pub(crate) fn exec_sync_args_without_command(&self) -> Result<Vec<String>> {
538        // Pre-allocate capacity for typical arg count
539        let mut args = Vec::with_capacity(12);
540        args.extend(self.default_args().context("build default runtime args")?);
541
542        if let Some(rr) = self.config.runtime_root() {
543            args.push(format!("--root={}", rr.display()));
544        }
545
546        if self.cgroup_manager == CgroupManager::Systemd {
547            args.push(Self::SYSTEMD_CGROUP_ARG.into());
548        }
549
550        // Use static strings to avoid allocations
551        args.push("exec".into());
552        args.push("-d".into());
553
554        if let ContainerIOType::Terminal(terminal) = self.container_io.typ() {
555            args.push(format!("--console-socket={}", terminal.path().display()));
556            args.push("--tty".into());
557        }
558
559        args.push(format!("--pid-file={}", self.pidfile.display()));
560        args.push(self.id.into());
561
562        Ok(args)
563    }
564
565    /// Build the default arguments for any provided runtime.
566    fn default_args(&self) -> Result<Vec<String>> {
567        let mut args = vec![];
568
569        if self
570            .config
571            .runtime()
572            .file_name()
573            .context("no filename in path")?
574            == Self::RUNTIME_CRUN
575        {
576            debug!("Found crun used as runtime");
577            args.push(format!("--log=journald:{}", self.id));
578
579            match self.config.log_level() {
580                &LogLevel::Debug | &LogLevel::Error => args.push(format!(
581                    "{}={}",
582                    Self::LOG_LEVEL_FLAG_CRUN,
583                    self.config.log_level()
584                )),
585                &LogLevel::Warn => args.push(format!("{}=warning", Self::LOG_LEVEL_FLAG_CRUN)),
586                _ => {}
587            }
588        }
589
590        if let Some(rr) = self.config.runtime_root() {
591            args.push(format!("--root={}", rr.display()));
592        }
593
594        if self.cgroup_manager == CgroupManager::Systemd {
595            args.push(Self::SYSTEMD_CGROUP_ARG.into());
596        }
597
598        Ok(args)
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605    use std::os::fd::AsRawFd;
606
607    #[test]
608    fn inherited_fds_reports_descriptors_above_stderr() -> Result<()> {
609        let file = tempfile::tempfile()?;
610        let fd = file.as_raw_fd();
611
612        let fds = Server::inherited_fds();
613
614        assert!(
615            fds.contains(&fd),
616            "expected an open descriptor {fd} to be reported, got {fds:?}"
617        );
618        // stdin, stdout and stderr are never reported, they are not ours to close.
619        assert!(!fds.contains(&0));
620        assert!(!fds.contains(&1));
621        assert!(!fds.contains(&2));
622
623        Ok(())
624    }
625}