From bf98c3c595e3f771b8c761599c2a76b0dc111b5f Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 23 Aug 2025 19:37:30 +0200 Subject: [PATCH] added healthcheck command - fixes #1432, fixes #1453 --- Cargo.lock | 86 ++++++++++++++++++-- Cargo.toml | 9 ++ docker/Dockerfile | 3 +- warpgate-common/src/config/mod.rs | 4 - warpgate-common/src/types/listen_endpoint.rs | 4 + warpgate-protocol-http/Cargo.toml | 8 +- warpgate-protocol-http/src/api/info.rs | 6 +- warpgate/Cargo.toml | 3 +- warpgate/src/commands/check.rs | 20 ++--- warpgate/src/commands/healthcheck.rs | 27 ++++++ warpgate/src/commands/mod.rs | 1 + warpgate/src/commands/run.rs | 18 ++-- warpgate/src/commands/setup.rs | 9 +- warpgate/src/logging.rs | 8 +- warpgate/src/main.rs | 3 + 15 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 warpgate/src/commands/healthcheck.rs diff --git a/Cargo.lock b/Cargo.lock index 46457259..f41e4b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,13 +197,29 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "asn1-rs" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" dependencies = [ - "asn1-rs-derive", + "asn1-rs-derive 0.6.0", "asn1-rs-impl", "displaydoc", "nom", @@ -213,6 +229,18 @@ dependencies = [ "time", ] +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + [[package]] name = "asn1-rs-derive" version = "0.6.0" @@ -1059,13 +1087,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der-parser" version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "displaydoc", "nom", "num-bigint", @@ -2676,13 +2718,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + [[package]] name = "oid-registry" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", ] [[package]] @@ -3516,6 +3567,7 @@ dependencies = [ "pem", "rustls-pki-types", "time", + "x509-parser 0.16.0", "yasna", "zeroize", ] @@ -5492,6 +5544,7 @@ dependencies = [ "futures", "notify", "rcgen", + "reqwest", "rustls", "schemars", "sd-notify", @@ -5583,7 +5636,7 @@ dependencies = [ "uuid", "warpgate-sso", "webpki", - "x509-parser", + "x509-parser 0.17.0", ] [[package]] @@ -5691,6 +5744,7 @@ dependencies = [ "poem-openapi", "regex", "reqwest", + "rustls-pemfile", "sea-orm", "serde", "serde_json", @@ -6391,18 +6445,36 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom", + "oid-registry 0.7.1", + "ring", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "x509-parser" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "data-encoding", - "der-parser", + "der-parser 10.0.0", "lazy_static", "nom", - "oid-registry", + "oid-registry 0.8.1", "rusticata-macros", "thiserror 2.0.12", "time", diff --git a/Cargo.toml b/Cargo.toml index 382f4c9d..25a6efef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,15 @@ rand_core = { version = "0.6", features = ["std"], default-features = false } dialoguer = { version = "0.11", default-features = false, features = ["editor", "password"] } tokio = { version = "1.20", features = ["tracing", "signal", "macros", "rt-multi-thread", "io-util"], default-features = false } governor = { version = "0.10.0", default-features = false, features = ["std", "quanta", "jitter"] } +rcgen = { version = "0.13", features = ["zeroize", "crypto", "aws_lc_rs", "pem", "x509-parser"], default-features = false } +x509-parser = { version = "0.17.0", default-features = false } +uuid = { version = "1.3", features = ["v4", "serde"], default-features = false } +reqwest = { version = "0.12", features = [ + "http2", # required for connecting to targets behind AWS ELB + "rustls-tls-native-roots-no-provider", + "stream", + "gzip", +], default-features = false } [profile.release] lto = true diff --git a/docker/Dockerfile b/docker/Dockerfile index 8e4bc68f..2ac1cca1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -47,11 +47,10 @@ COPY --from=build /opt/warpgate/target/release/warpgate /usr/local/bin/warpgate VOLUME /data -HEALTHCHECK CMD wget --no-verbose --tries=1 --no-check-certificate --spider http://localhost:8888/@warpgate/api/info || exit 1 +HEALTHCHECK CMD warpgate healthcheck ENV DOCKER=1 USER warpgate ENTRYPOINT ["warpgate", "--config", "/data/warpgate.yaml"] CMD ["run"] - diff --git a/warpgate-common/src/config/mod.rs b/warpgate-common/src/config/mod.rs index fe5c8733..718b333b 100644 --- a/warpgate-common/src/config/mod.rs +++ b/warpgate-common/src/config/mod.rs @@ -226,9 +226,6 @@ pub struct SniCertificateConfig { #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct HttpConfig { - #[serde(default = "_default_false")] - pub enable: bool, - #[serde(default = "_default_http_listen")] pub listen: ListenEndpoint, @@ -259,7 +256,6 @@ pub struct HttpConfig { impl Default for HttpConfig { fn default() -> Self { HttpConfig { - enable: false, listen: _default_http_listen(), external_port: None, certificate: "".to_owned(), diff --git a/warpgate-common/src/types/listen_endpoint.rs b/warpgate-common/src/types/listen_endpoint.rs index 9e8b2de1..779748e4 100644 --- a/warpgate-common/src/types/listen_endpoint.rs +++ b/warpgate-common/src/types/listen_endpoint.rs @@ -16,6 +16,10 @@ use crate::WarpgateError; pub struct ListenEndpoint(SocketAddr); impl ListenEndpoint { + pub fn address(&self) -> SocketAddr { + self.0 + } + pub fn addresses_to_listen_on(&self) -> Result, WarpgateError> { // For [::], explicitly return both addresses so that we are not affected // by the state of the ipv6only sysctl. diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index 93bce0e9..30a0bd6e 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -16,12 +16,8 @@ http = { version = "1.0", default-features = false } once_cell = { version = "1.17", default-features = false } poem.workspace = true poem-openapi.workspace = true -reqwest = { version = "0.12", features = [ - "http2", # required for connecting to targets behind AWS ELB - "rustls-tls-native-roots-no-provider", - "stream", - "gzip", -], default-features = false } +reqwest.workspace = true +rustls-pemfile.workspace = true sea-orm.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/warpgate-protocol-http/src/api/info.rs b/warpgate-protocol-http/src/api/info.rs index c6cfd4d1..18ccb506 100644 --- a/warpgate-protocol-http/src/api/info.rs +++ b/warpgate-protocol-http/src/api/info.rs @@ -124,11 +124,7 @@ impl Api { } else { None }, - http: if config.store.http.enable { - Some(config.store.http.external_port()) - } else { - None - }, + http: Some(config.store.http.external_port()), mysql: if config.store.mysql.enable { Some(config.store.mysql.external_port()) } else { diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index bcf44fe6..3fea4a3e 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -18,7 +18,8 @@ dialoguer.workspace = true enum_dispatch.workspace = true futures.workspace = true notify = { version = "8.0", default-features = false, features = ["fsevent-sys"] } -rcgen = { version = "0.13", features = ["zeroize", "crypto", "aws_lc_rs", "pem"], default-features = false } +rcgen.workspace = true +reqwest.workspace = true rustls.workspace = true serde_json.workspace = true serde_yaml = { version = "0.9", default-features = false } diff --git a/warpgate/src/commands/check.rs b/warpgate/src/commands/check.rs index 9c62e03f..b7a9abfd 100644 --- a/warpgate/src/commands/check.rs +++ b/warpgate/src/commands/check.rs @@ -6,18 +6,16 @@ use crate::config::load_config; pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { let config = load_config(&cli.config, true)?; - if config.store.http.enable { - TlsCertificateBundle::from_file( - config - .paths_relative_to - .join(&config.store.http.certificate), - ) + TlsCertificateBundle::from_file( + config + .paths_relative_to + .join(&config.store.http.certificate), + ) + .await + .with_context(|| "Checking HTTPS certificate".to_string())?; + TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.http.key)) .await - .with_context(|| "Checking HTTPS certificate".to_string())?; - TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.http.key)) - .await - .with_context(|| "Checking HTTPS key".to_string())?; - } + .with_context(|| "Checking HTTPS key".to_string())?; if config.store.mysql.enable { TlsCertificateBundle::from_file( config diff --git a/warpgate/src/commands/healthcheck.rs b/warpgate/src/commands/healthcheck.rs new file mode 100644 index 00000000..d6a88f60 --- /dev/null +++ b/warpgate/src/commands/healthcheck.rs @@ -0,0 +1,27 @@ +use anyhow::{Context, Result}; +use tokio::time::timeout; + +use crate::config::load_config; + +pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { + let config = load_config(&cli.config, true)?; + + let url = format!( + "https://{}/@warpgate/api/info", + config.store.http.listen.address() + ); + + let client = reqwest::Client::builder() + .danger_accept_invalid_certs(true) + .use_rustls_tls() + .build()?; + + let response = timeout(std::time::Duration::from_secs(5), client.get(&url).send()) + .await + .context("Timeout")? + .context("Failed to send request")?; + + response.error_for_status()?; + + Ok(()) +} diff --git a/warpgate/src/commands/mod.rs b/warpgate/src/commands/mod.rs index 474f45bb..fa8d8122 100644 --- a/warpgate/src/commands/mod.rs +++ b/warpgate/src/commands/mod.rs @@ -1,6 +1,7 @@ pub mod check; pub mod client_keys; mod common; +pub mod healthcheck; pub mod recover_access; pub mod run; pub mod setup; diff --git a/warpgate/src/commands/run.rs b/warpgate/src/commands/run.rs index 86b0a526..bba6c7fa 100644 --- a/warpgate/src/commands/run.rs +++ b/warpgate/src/commands/run.rs @@ -60,6 +60,14 @@ pub(crate) async fn command(cli: &crate::Cli, enable_admin_token: bool) -> Resul let mut protocol_futures = futures::stream::FuturesUnordered::new(); + protocol_futures.push( + run_protocol_server( + HTTPProtocolServer::new(&services).await?, + config.store.http.listen.clone(), + ) + .boxed(), + ); + if config.store.ssh.enable { protocol_futures.push( run_protocol_server( @@ -70,16 +78,6 @@ pub(crate) async fn command(cli: &crate::Cli, enable_admin_token: bool) -> Resul ); } - if config.store.http.enable { - protocol_futures.push( - run_protocol_server( - HTTPProtocolServer::new(&services).await?, - config.store.http.listen.clone(), - ) - .boxed(), - ); - } - if config.store.mysql.enable { protocol_futures.push( run_protocol_server( diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index 9442b7c0..52a014ae 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -74,13 +74,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { ); let theme = ColorfulTheme::default(); - let mut store = WarpgateConfigStore { - http: HttpConfig { - enable: true, - ..Default::default() - }, - ..Default::default() - }; + let mut store = WarpgateConfigStore::default(); // --- @@ -138,7 +132,6 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { } }); - store.http.enable = true; if let Commands::UnattendedSetup { http_port, .. } = &cli.command { store.http.listen = ListenEndpoint::from(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), *http_port)); diff --git a/warpgate/src/logging.rs b/warpgate/src/logging.rs index d09c8828..ed4fae6f 100644 --- a/warpgate/src/logging.rs +++ b/warpgate/src/logging.rs @@ -28,10 +28,10 @@ pub async fn init_logging(config: Option<&WarpgateConfig>, cli: &Cli) { let registry = tracing_subscriber::registry(); - #[cfg(all(debug_assertions, feature = "tokio-console"))] - let console_layer = console_subscriber::spawn(); - #[cfg(all(debug_assertions, feature = "tokio-console"))] - let registry = registry.with(console_layer); + // #[cfg(all(debug_assertions, feature = "tokio-console"))] + // let console_layer = console_subscriber::spawn(); + // #[cfg(all(debug_assertions, feature = "tokio-console"))] + // let registry = registry.with(console_layer); let socket_layer = match config { Some(config) => Some(make_socket_logger_layer(config).await), diff --git a/warpgate/src/main.rs b/warpgate/src/main.rs index e8ced876..a8a4536f 100644 --- a/warpgate/src/main.rs +++ b/warpgate/src/main.rs @@ -93,6 +93,8 @@ pub(crate) enum Commands { }, /// Show version information Version, + /// Automatic healthcheck for running Warpgate in a container + Healthcheck, } async fn _main() -> Result<()> { @@ -124,6 +126,7 @@ async fn _main() -> Result<()> { Commands::RecoverAccess { username } => { crate::commands::recover_access::command(&cli, username).await } + Commands::Healthcheck => crate::commands::healthcheck::command(&cli).await, } }