Skip to main content

malwaredb_server/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![doc = include_str!("../README.md")]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![deny(missing_docs)]
6#![deny(clippy::all)]
7#![deny(clippy::pedantic)]
8
9/// Cryptographic functionality for file storage
10pub mod crypto;
11
12/// Database I/O
13pub mod db;
14
15/// HTTP Server
16pub mod http;
17
18/// Entropy functions
19pub mod utils;
20
21/// Virus Total integration
22#[cfg_attr(docsrs, doc(cfg(feature = "vt")))]
23#[cfg(feature = "vt")]
24pub mod vt;
25
26/// Yara-related functionality
27#[cfg_attr(docsrs, doc(cfg(feature = "yara")))]
28#[cfg(feature = "yara")]
29pub mod yara;
30
31use crate::crypto::FileEncryption;
32use crate::db::MDBConfig;
33use malwaredb_api::ServerInfo;
34//use utils::HashPath;
35
36use std::collections::HashMap;
37use std::fmt::{Debug, Formatter};
38use std::io::{Cursor, Read};
39use std::net::{IpAddr, Ipv4Addr, SocketAddr};
40use std::path::PathBuf;
41#[cfg(feature = "admin")]
42use std::sync::atomic::{AtomicBool, AtomicU64};
43use std::sync::{Arc, LazyLock};
44use std::time::{Duration, SystemTime};
45
46use anyhow::{Context, Result, anyhow, bail, ensure};
47use axum_server::tls_rustls::RustlsConfig;
48use chrono::Local;
49use chrono_humanize::{Accuracy, HumanTime, Tense};
50use flate2::read::GzDecoder;
51use mdns_sd::{ServiceDaemon, ServiceInfo};
52use sha2::{Digest, Sha256};
53use tokio::net::TcpListener;
54use tracing::{trace, warn};
55
56/// MDB version
57pub const MDB_VERSION: &str = env!("CARGO_PKG_VERSION");
58
59/// MDB version as a semantic version object
60pub static MDB_VERSION_SEMVER: LazyLock<semver::Version> =
61    LazyLock::new(|| semver::Version::parse(MDB_VERSION).unwrap());
62
63/// How often stale pagination searches should be cleaned up: 1 day
64/// Could be used if future cleanup operations are needed.
65pub(crate) const DB_CLEANUP_INTERVAL: Duration = Duration::from_hours(24);
66
67/// Gzip's magic number to see if a file is compressed
68pub const GZIP_MAGIC: [u8; 2] = [0x1fu8, 0x8bu8];
69
70/// Zstd magic number to see if a file is compressed
71pub const ZSTD_MAGIC: [u8; 4] = [0x28u8, 0xb5u8, 0x2fu8, 0xfdu8];
72
73/// Builder for server configuration
74pub struct StateBuilder {
75    /// The port which will be used to listen for connections.
76    pub port: u16,
77
78    /// The directory to store malware samples if we're keeping them.
79    pub directory: Option<PathBuf>,
80
81    /// Maximum upload size
82    pub max_upload: usize,
83
84    /// The IP to use for listening for connections
85    pub ip: IpAddr,
86
87    /// Handle to the database connection
88    db_type: db::DatabaseType,
89
90    /// Virus Total API key
91    #[cfg(feature = "vt")]
92    vt_client: Option<malwaredb_virustotal::VirusTotalClient>,
93
94    /// TLS configuration constructed from certificate and private key files
95    tls_config: Option<RustlsConfig>,
96
97    /// If Malware DB should be advertised via Multicast DNS (also known as Bonjour or Zeroconf)
98    mdns: bool,
99}
100
101impl StateBuilder {
102    /// Create the builder starting with the database configuration, and optionally, the
103    /// certificate for communicating with Postgres.
104    ///
105    /// # Errors
106    ///
107    /// An error occurs if the database configuration isn't valid or if an error occurs connecting
108    /// to the database.
109    pub async fn new(db_string: &str, pg_cert: Option<PathBuf>) -> Result<Self> {
110        let db_type = db::DatabaseType::from_string(db_string, pg_cert).await?;
111
112        Ok(Self {
113            port: 8080,
114            directory: None,
115            max_upload: 104_857_600, /* 100 MiB */
116            ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
117            db_type,
118            #[cfg(feature = "vt")]
119            vt_client: None,
120            tls_config: None,
121            mdns: false,
122        })
123    }
124
125    /// Specify the port to listen on.
126    /// Default: `8080`
127    #[must_use]
128    pub fn port(mut self, port: u16) -> Self {
129        self.port = port;
130        self
131    }
132
133    /// Specify the directory to store malware samples if we're keeping them.
134    ///
135    /// Default: No directory, no file saving
136    #[must_use]
137    pub fn directory(mut self, directory: PathBuf) -> Self {
138        self.directory = Some(directory);
139        self
140    }
141
142    /// Specify the maximum upload size in bytes.
143    /// Default is 100 MiB.
144    #[must_use]
145    pub fn max_upload(mut self, max_upload: usize) -> Self {
146        self.max_upload = max_upload;
147        self
148    }
149
150    /// Indicate the IP address the server will list on.
151    /// Default: 127.0.0.1
152    #[must_use]
153    pub fn ip(mut self, ip: IpAddr) -> Self {
154        self.ip = ip;
155        self
156    }
157
158    /// Provide the Virus Total API key.
159    #[must_use]
160    #[cfg(feature = "vt")]
161    #[cfg_attr(docsrs, doc(cfg(feature = "vt")))]
162    pub fn vt_client(mut self, vt_client: malwaredb_virustotal::VirusTotalClient) -> Self {
163        self.vt_client = Some(vt_client);
164        self
165    }
166
167    /// Provide the certificate and private key for TLS mode.
168    /// Files must match: both as PEM or both as DER.
169    ///
170    /// # Errors
171    ///
172    /// An error results if either file doesn't exist, not in the same format, or cannot be parsed.
173    pub async fn tls(mut self, cert_file: PathBuf, key_file: PathBuf) -> Result<Self> {
174        ensure!(cert_file.exists(), "Certificate file {} does not exist!", cert_file.display());
175
176        ensure!(key_file.exists(), "Key file {} does not exist!", key_file.display());
177
178        let cert_ext_str = cert_file
179            .extension()
180            .context("failed to get certificate extension")?;
181        let key_ext_str = key_file
182            .extension()
183            .context("failed to get key extension")?;
184
185        // Unnecessary for running MalwareDB, but some unit tests fail without this check.
186        if rustls::crypto::CryptoProvider::get_default().is_none() {
187            rustls::crypto::aws_lc_rs::default_provider()
188                .install_default()
189                .map_err(|_| anyhow!("failed to install AWS-LC crypto provider"))?;
190        }
191
192        let config = if (cert_ext_str == "pem" || cert_ext_str == "crt") && key_ext_str == "pem" {
193            RustlsConfig::from_pem_file(cert_file, key_file)
194                .await
195                .context("failed to load or parse certificate and key pem files")?
196        } else if cert_ext_str == "der" && key_ext_str == "der" {
197            let cert_contents =
198                std::fs::read(cert_file).context("failed to read certificate file")?;
199            let key_contents =
200                std::fs::read(key_file).context("failed to read private key file")?;
201            RustlsConfig::from_der(vec![cert_contents], key_contents)
202                .await
203                .context("failed to parse certificate and key der files")?
204        } else {
205            bail!(
206                "Unknown or unmatched certificate and key file extensions {} and {}",
207                cert_ext_str.display(),
208                key_ext_str.display()
209            );
210        };
211
212        self.tls_config = Some(config);
213        Ok(self)
214    }
215
216    /// Indicate that Malware DB should advertise itself via multicast DNS.
217    /// Default is false.
218    #[must_use]
219    pub fn enable_mdns(mut self) -> Self {
220        self.mdns = true;
221        self
222    }
223
224    /// Generate the state object.
225    ///
226    /// # Errors
227    ///
228    /// An error occurs if the database can't be reached.
229    pub async fn into_state(self) -> Result<State> {
230        let db_config = self.db_type.get_config().await?;
231        let keys = self.db_type.get_encryption_keys().await?;
232
233        Ok(State {
234            port: self.port,
235            directory: self.directory,
236            max_upload: self.max_upload,
237            ip: self.ip,
238            db_type: Arc::new(self.db_type),
239            started: SystemTime::now(),
240            db_config,
241            keys,
242            #[cfg(feature = "vt")]
243            vt_client: self.vt_client,
244            tls_config: self.tls_config,
245            mdns: if self.mdns {
246                Some(ServiceDaemon::new()?)
247            } else {
248                None
249            },
250        })
251    }
252}
253
254/// State & configuration of the running server instance
255pub struct State {
256    /// The port which will be used to listen for connections.
257    pub port: u16,
258
259    /// The directory to store malware samples if we're keeping them.
260    pub directory: Option<PathBuf>,
261
262    /// Maximum upload size
263    pub max_upload: usize,
264
265    /// The IP to use for listening for connections
266    pub ip: IpAddr,
267
268    /// Handle to the database connection
269    pub db_type: Arc<db::DatabaseType>,
270
271    /// Start time of the server
272    pub started: SystemTime,
273
274    /// Configuration which is stored in the database
275    pub db_config: MDBConfig,
276
277    /// File encryption keys, may be empty
278    pub(crate) keys: HashMap<u32, FileEncryption>,
279
280    /// Virus Total API key
281    #[cfg(feature = "vt")]
282    pub(crate) vt_client: Option<malwaredb_virustotal::VirusTotalClient>,
283
284    /// TLS configuration constructed from certificate and private key files
285    tls_config: Option<RustlsConfig>,
286
287    /// If Malware DB should be advertised via Multicast DNS (also known as Bonjour or Zeroconf)
288    mdns: Option<ServiceDaemon>,
289}
290
291impl State {
292    /// Store the sample with a depth of three based on the sample's SHA-256 hash, even if compressed
293    ///
294    /// # Errors
295    ///
296    /// * If the file can't be written.
297    /// * If a necessary sub-directory can't be created.
298    pub async fn store_bytes(&self, data: &[u8]) -> Result<bool> {
299        if let Some(dest_path) = &self.directory {
300            let mut hasher = Sha256::new();
301            hasher.update(data);
302            let sha256 = hex::encode(hasher.finalize());
303
304            // Trait `HashPath` needs to be re-worked so it can work with Strings.
305            // This code below ends up making the String into ASCII representations of the hash
306            // See: https://github.com/malwaredb/malwaredb-rs/issues/60
307            let hashed_path =
308                format!("{}/{}/{}/{}", &sha256[0..2], &sha256[2..4], &sha256[4..6], sha256);
309
310            // The path which has the file name included, with the storage directory prepended.
311            //let hashed_path = result.hashed_path(3);
312            let mut dest_path = dest_path.clone();
313            dest_path.push(hashed_path);
314
315            // Remove the file name so we can just have the directory path.
316            let mut just_the_dir = dest_path.clone();
317            just_the_dir.pop();
318            std::fs::create_dir_all(just_the_dir)?;
319
320            let data = if self.db_config.compression {
321                let buff = Cursor::new(data);
322                let mut compressed = Vec::with_capacity(data.len() / 2);
323                zstd::stream::copy_encode(buff, &mut compressed, 4)?;
324                compressed
325            } else {
326                data.to_vec()
327            };
328
329            self.db_type.clear_file_crypto(&sha256).await?; // In case we're re-writing
330            let data = if let Some(key_id) = self.db_config.default_key {
331                if let Some(key) = self.keys.get(&key_id) {
332                    let nonce = key.nonce();
333                    self.db_type
334                        .set_file_nonce(&sha256, nonce.as_deref())
335                        .await?;
336                    key.encrypt(&data, nonce)?
337                } else {
338                    bail!("Key not available!")
339                }
340            } else {
341                data
342            };
343
344            std::fs::write(dest_path, data)?;
345
346            Ok(true)
347        } else {
348            Ok(false)
349        }
350    }
351
352    /// Retrieve a sample given the SHA-256 hash
353    /// Assumes that `MalwareDB` permissions have already been checked to ensure this is permitted.
354    ///
355    /// # Errors
356    ///
357    /// * The file could not be read, maybe because it doesn't exist.
358    /// * Failure to decrypt or decompress (corruption).
359    pub async fn retrieve_bytes(&self, sha256: &String) -> Result<Vec<u8>> {
360        if let Some(dest_path) = &self.directory {
361            let path = format!("{}/{}/{}/{}", &sha256[0..2], &sha256[2..4], &sha256[4..6], sha256);
362            // Trait `HashPath` needs to be re-worked so it can work with Strings.
363            // This code below ends up making the String into ASCII representations of the hash
364            // See: https://github.com/malwaredb/malwaredb-rs/issues/60
365            //let path = sha256.as_bytes().iter().hashed_path(3);
366            let contents = std::fs::read(dest_path.join(path))?;
367
368            let contents = if self.keys.is_empty() {
369                // We don't have file encryption enabled
370                contents
371            } else {
372                let (key_id, nonce) = self.db_type.get_file_encryption_key_id(sha256).await?;
373                if let Some(key_id) = key_id {
374                    if let Some(key) = self.keys.get(&key_id) {
375                        key.decrypt(&contents, nonce)?
376                    } else {
377                        bail!("File was encrypted but we don't have tke key!")
378                    }
379                } else {
380                    // File was not encrypted
381                    contents
382                }
383            };
384
385            if contents.starts_with(&GZIP_MAGIC) {
386                let buff = Cursor::new(contents);
387                let mut decompressor = GzDecoder::new(buff);
388                let mut decompressed: Vec<u8> = vec![];
389                decompressor.read_to_end(&mut decompressed)?;
390                Ok(decompressed)
391            } else if contents.starts_with(&ZSTD_MAGIC) {
392                let buff = Cursor::new(contents);
393                let mut decompressed: Vec<u8> = vec![];
394                zstd::stream::copy_decode(buff, &mut decompressed)?;
395                Ok(decompressed)
396            } else {
397                Ok(contents)
398            }
399        } else {
400            bail!("files are not saved")
401        }
402    }
403
404    /// Get the duration for which the server has been running
405    ///
406    /// # Panics
407    ///
408    /// Despite the `unwrap()` this function will not panic as the data used is guaranteed to be valid.
409    #[must_use]
410    pub fn since(&self) -> Duration {
411        let now = SystemTime::now();
412        now.duration_since(self.started).unwrap()
413    }
414
415    /// Get server information
416    ///
417    /// # Errors
418    ///
419    /// An error would occur if the Postgres server could not be reached.
420    pub async fn get_info(&self) -> Result<ServerInfo> {
421        let db_info = self.db_type.db_info().await?;
422        let uptime = Local::now() - self.since();
423        let mem_size = app_memory_usage_fetcher::get_memory_usage_string().unwrap_or_default();
424
425        Ok(ServerInfo {
426            os_name: std::env::consts::OS.into(),
427            memory_used: mem_size,
428            num_samples: db_info.num_files,
429            num_users: db_info.num_users,
430            uptime: HumanTime::from(uptime).to_text_en(Accuracy::Rough, Tense::Present),
431            mdb_version: MDB_VERSION_SEMVER.clone(),
432            db_version: db_info.version,
433            db_size: db_info.size,
434            instance_name: self.db_config.name.clone(),
435            vt_support: cfg!(feature = "vt"),
436            yara_enabled: cfg!(feature = "yara"),
437        })
438    }
439
440    /// Returns a list of encryption keys as types and IDs by querying the database each time. This
441    /// allows for key additions to be shown without restarting the GUI.
442    ///
443    /// # Errors
444    ///
445    /// Database connectivity errors could occur.
446    #[inline]
447    #[cfg(feature = "admin")]
448    #[cfg_attr(docsrs, doc(cfg(feature = "admin")))]
449    pub async fn encryption_keys(&self) -> Result<HashMap<u32, crypto::EncryptionOption>> {
450        Ok(self
451            .db_type
452            .get_encryption_keys()
453            .await?
454            .iter()
455            .map(|(id, key)| (*id, key.key_type()))
456            .collect())
457    }
458
459    /// The server listens and responds to requests. Does not return unless there's an error.
460    ///
461    /// # Errors
462    ///
463    /// * If the certificate and private key could not be parsed or are not valid.
464    /// * If the IP address and port are already in use.
465    /// * If the service doesn't have permission to open the port.
466    pub async fn serve(
467        self,
468        #[cfg(target_family = "windows")] rx: Option<tokio::sync::mpsc::Receiver<()>>,
469    ) -> Result<()> {
470        let socket = SocketAddr::new(self.ip, self.port);
471        let arc_self = Arc::new(self);
472        let db_info = arc_self.db_type.clone();
473
474        #[cfg(feature = "yara")]
475        {
476            if arc_self.directory.is_some() {
477                start_yara_process(arc_self.clone());
478            }
479        }
480
481        tokio::spawn(async move {
482            loop {
483                match db_info.cleanup().await {
484                    Ok(removed) => {
485                        trace!("Pagination cleanup succeeded, {removed} searches removed");
486                    }
487                    Err(e) => warn!("Pagination cleanup failed: {e}"),
488                }
489
490                tokio::time::sleep(DB_CLEANUP_INTERVAL).await;
491            }
492        });
493
494        if let Some(mdns) = &arc_self.mdns {
495            let host_name = format!("{}.local.", arc_self.ip);
496            let ssl = arc_self.tls_config.is_some();
497            #[cfg(not(feature = "anonymous"))]
498            let properties = [("ssl", ssl.to_string()), ("version", MDB_VERSION.into())];
499            #[cfg(feature = "anonymous")]
500            let properties = if arc_self.db_config.anonymous_uid.is_some() {
501                [
502                    ("ssl", ssl.to_string()),
503                    ("version", MDB_VERSION.into()),
504                    ("anonymous", "true".into()),
505                ]
506                .to_vec()
507            } else {
508                [("ssl", ssl.to_string()), ("version", MDB_VERSION.into())].to_vec()
509            };
510            let service = {
511                let mut service = ServiceInfo::new(
512                    malwaredb_api::MDNS_NAME,
513                    &arc_self.db_config.name,
514                    &host_name,
515                    &arc_self.ip,
516                    arc_self.port,
517                    &properties[..],
518                )?;
519                if arc_self.ip.is_unspecified() {
520                    service = service.enable_addr_auto();
521                }
522                service
523            };
524            trace!("Registering MDNS service...");
525            mdns.register(service)?;
526        }
527
528        if let Some(tls_config) = arc_self.tls_config.clone() {
529            println!("Listening on https://{socket:?}");
530            let handle = axum_server::Handle::<SocketAddr>::new();
531            let server_future = axum_server::bind_rustls(socket, tls_config)
532                .serve(http::app(arc_self).into_make_service());
533            tokio::select! {
534                () = shutdown_signal(#[cfg(target_family = "windows")]rx) =>
535                    handle.graceful_shutdown(Some(Duration::from_secs(30))),
536                res = server_future => res?,
537            }
538            warn!("Terminate signal received");
539        } else {
540            println!("Listening on http://{socket:?}");
541            let listener = TcpListener::bind(socket)
542                .await
543                .context(format!("failed to bind socket {socket}"))?;
544            axum::serve(listener, http::app(arc_self).into_make_service())
545                .with_graceful_shutdown(shutdown_signal(
546                    #[cfg(target_family = "windows")]
547                    rx,
548                ))
549                .await?;
550            warn!("Terminate signal received");
551        }
552        Ok(())
553    }
554
555    /// Re-write files so that configuration changes are applied.
556    /// * Compression enabled or disabled.
557    /// * Encryption enabled or disabled.
558    ///
559    /// `rewritten_counter` allows for a future GUI to see progress.
560    /// `stop_signal` allows the calling function to stop the process early.
561    ///
562    /// # Errors
563    ///
564    /// May return an error if a file cannot be read, written, or moved.
565    #[cfg(feature = "admin")]
566    pub async fn rewrite_files(
567        &self,
568        rewritten_counter: Arc<AtomicU64>,
569        stop_signal: Arc<AtomicBool>,
570    ) -> Result<bool> {
571        use std::sync::atomic::Ordering;
572
573        let Some(dest_path) = &self.directory else {
574            return Ok(false);
575        };
576
577        for entry in walkdir::WalkDir::new(dest_path)
578            .follow_links(false)
579            .max_depth(4)
580            .into_iter()
581            .flatten()
582        {
583            if stop_signal.load(Ordering::Relaxed) {
584                return Ok(true);
585            }
586
587            if entry.file_type().is_file() {
588                let file_name = entry
589                    .file_name()
590                    .to_str()
591                    .ok_or_else(|| anyhow!("Invalid file name for path: {entry:?}"))?
592                    .to_string();
593                ensure!(
594                    file_name.len() == 64,
595                    "File name must be 64 characters to be a SHA-256 hash"
596                );
597
598                // Check the file
599                let mut header = vec![0; ZSTD_MAGIC.len()];
600                let mut file = std::fs::File::open(entry.path())?;
601                file.read_exact(&mut header)?;
602
603                let do_compression = self.db_config.compression != (header == ZSTD_MAGIC);
604                let do_encryption = self.db_config.default_key
605                    != self.db_type.get_file_encryption_key_id(&file_name).await?.0;
606
607                // Nothing to do
608                if !do_compression || !do_encryption {
609                    continue;
610                }
611
612                // Make the requested changes
613                let contents = self.retrieve_bytes(&file_name).await?;
614                self.store_bytes(&contents).await?;
615
616                rewritten_counter.fetch_add(1, Ordering::Relaxed);
617            }
618        }
619        Ok(true)
620    }
621}
622
623impl Debug for State {
624    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
625        let tls_mode = if self.tls_config.is_some() {
626            ", TLS mode"
627        } else {
628            ""
629        };
630        write!(f, "MDB state, port {}, database {:?}{tls_mode}", self.port, self.db_type)
631    }
632}
633
634#[cfg(feature = "yara")]
635#[allow(clippy::needless_pass_by_value)]
636fn start_yara_process(state: Arc<State>) {
637    let state_clone = state.clone();
638    tokio::spawn(async move {
639        let state_clone = state_clone.clone();
640        loop {
641            let tasks = match state_clone
642                .clone()
643                .db_type
644                .get_unfinished_yara_tasks()
645                .await
646            {
647                Ok(tasks) => tasks,
648                Err(e) => {
649                    warn!("Failed to get Yara tasks: {e}");
650                    continue;
651                }
652            };
653            for task in tasks {
654                let state_clone = state_clone.clone();
655                let (hashes, last_file_id) = match state_clone
656                    .db_type
657                    .user_allowed_files_by_sha256(task.user_id, task.last_file_id)
658                    .await
659                {
660                    Ok(hashes) => hashes,
661                    Err(e) => {
662                        warn!("Failed to get user allowed files: {e}");
663                        continue;
664                    }
665                };
666
667                if hashes.is_empty() {
668                    if let Err(e) = state_clone
669                        .db_type
670                        .mark_yara_task_as_finished(task.id)
671                        .await
672                    {
673                        warn!("Failed to mark yara task as finished: {e}");
674                    }
675                    continue;
676                }
677                tokio::spawn(async move {
678                    for hash in hashes {
679                        let bytes = match state_clone.clone().retrieve_bytes(&hash).await {
680                            Ok(bytes) => bytes,
681                            Err(e) => {
682                                warn!("Failed to retrieve bytes for hash {hash}: {e}");
683                                continue;
684                            }
685                        };
686                        let matches = task.process_yara_rules(&bytes).unwrap_or_else(|e| {
687                            warn!("Failed to process Yara rules: {e}");
688                            Vec::new()
689                        });
690
691                        for match_ in matches {
692                            if let Err(e) = state_clone
693                                .db_type
694                                .add_yara_match(task.id, &match_, &hash)
695                                .await
696                            {
697                                warn!("Failed to add Yara match: {e}");
698                            }
699                        }
700                    }
701                    if let Err(e) = state_clone
702                        .db_type
703                        .yara_add_next_file_id(task.id, last_file_id)
704                        .await
705                    {
706                        warn!("Failed to update yara task next file id: {e}");
707                    }
708                });
709            }
710            tokio::time::sleep(Duration::from_secs(5)).await;
711        }
712    });
713}
714
715/// Enable graceful shutdown
716/// <https://github.com/tokio-rs/axum/discussions/1500>
717async fn shutdown_signal(
718    #[cfg(target_family = "windows")] mut rx: Option<tokio::sync::mpsc::Receiver<()>>,
719) {
720    let ctrl_c = async {
721        tokio::signal::ctrl_c()
722            .await
723            .expect("failed to install Ctrl+C handler");
724    };
725
726    #[cfg(unix)]
727    let terminate = async {
728        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
729            .expect("failed to install signal handler")
730            .recv()
731            .await;
732    };
733
734    #[cfg(not(unix))]
735    let terminate = std::future::pending::<()>();
736
737    #[cfg(target_family = "windows")]
738    if let Some(rx_inner) = &mut rx {
739        let terminate_rx = rx_inner.recv();
740
741        tokio::select! {
742            () = ctrl_c => {},
743            () = terminate => {},
744            Some(()) = terminate_rx => {},
745        }
746    } else {
747        tokio::select! {
748            () = ctrl_c => {},
749            () = terminate => {},
750        }
751    }
752
753    #[cfg(not(target_family = "windows"))]
754    tokio::select! {
755        () = ctrl_c => {},
756        () = terminate => {},
757    }
758}