From 564c62591f8c2949d27658ef708e0144f328f6e7 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 9 Dec 2025 08:05:05 +0000 Subject: [PATCH] certbot: Support config for max DNS wait time --- certbot/cli/src/main.rs | 5 ++++ certbot/src/acme_client.rs | 30 ++++++++++++++++++++++-- certbot/src/acme_client/tests.rs | 2 +- certbot/src/bot.rs | 5 ++-- certbot/src/bot/tests.rs | 1 + gateway/dstack-app/builder/entrypoint.sh | 1 + gateway/dstack-app/deploy-to-vmm.sh | 3 +++ gateway/gateway.toml | 1 + gateway/src/config.rs | 4 ++++ 9 files changed, 47 insertions(+), 5 deletions(-) diff --git a/certbot/cli/src/main.rs b/certbot/cli/src/main.rs index 083ab641..5822853f 100644 --- a/certbot/cli/src/main.rs +++ b/certbot/cli/src/main.rs @@ -74,6 +74,8 @@ struct Config { renew_days_before: u64, /// Renew timeout in seconds renew_timeout: u64, + /// Maximum time to wait for DNS propagation in seconds + max_dns_wait: u64, /// Command to run after renewal #[serde(default)] renewed_hook: Option, @@ -91,6 +93,7 @@ impl Default for Config { renew_interval: 3600, renew_days_before: 10, renew_timeout: 120, + max_dns_wait: 300, renewed_hook: None, } } @@ -125,6 +128,7 @@ fn load_config(config: &PathBuf) -> Result { let renew_interval = Duration::from_secs(config.renew_interval); let renew_expires_in = Duration::from_secs(config.renew_days_before * 24 * 60 * 60); let renew_timeout = Duration::from_secs(config.renew_timeout); + let max_dns_wait = Duration::from_secs(config.max_dns_wait); let bot_config = CertBotConfig::builder() .acme_url(config.acme_url) .cert_dir(workdir.backup_dir()) @@ -137,6 +141,7 @@ fn load_config(config: &PathBuf) -> Result { .renew_interval(renew_interval) .renew_timeout(renew_timeout) .renew_expires_in(renew_expires_in) + .max_dns_wait(max_dns_wait) .credentials_file(workdir.account_credentials_path()) .auto_set_caa(config.auto_set_caa) .maybe_renewed_hook(config.renewed_hook) diff --git a/certbot/src/acme_client.rs b/certbot/src/acme_client.rs index f5ae739f..3650005e 100644 --- a/certbot/src/acme_client.rs +++ b/certbot/src/acme_client.rs @@ -27,6 +27,7 @@ pub struct AcmeClient { account: Account, credentials: Credentials, dns01_client: Dns01Client, + max_dns_wait: Duration, } #[derive(Debug, Clone)] @@ -53,7 +54,11 @@ pub(crate) fn acme_matches(encoded_credentials: &str, acme_url: &str) -> bool { } impl AcmeClient { - pub async fn load(dns01_client: Dns01Client, encoded_credentials: &str) -> Result { + pub async fn load( + dns01_client: Dns01Client, + encoded_credentials: &str, + max_dns_wait: Duration, + ) -> Result { let credentials: Credentials = serde_json::from_str(encoded_credentials)?; let account = Account::from_credentials(credentials.credentials).await?; let credentials: Credentials = serde_json::from_str(encoded_credentials)?; @@ -61,11 +66,16 @@ impl AcmeClient { account, dns01_client, credentials, + max_dns_wait, }) } /// Create a new account. - pub async fn new_account(acme_url: &str, dns01_client: Dns01Client) -> Result { + pub async fn new_account( + acme_url: &str, + dns01_client: Dns01Client, + max_dns_wait: Duration, + ) -> Result { let (account, credentials) = Account::create( &NewAccount { contact: &[], @@ -86,6 +96,7 @@ impl AcmeClient { account, dns01_client, credentials, + max_dns_wait, }) } @@ -335,6 +346,8 @@ impl AcmeClient { /// Self check the TXT records for the given challenges. async fn check_dns(&self, challenges: &[Challenge]) -> Result<()> { + use tracing::warn; + let mut delay = Duration::from_millis(250); let mut tries = 1u8; @@ -342,11 +355,22 @@ impl AcmeClient { debug!("Unsettled challenges: {unsettled_challenges:#?}"); + let start_time = std::time::Instant::now(); + 'outer: loop { use hickory_resolver::AsyncResolver; sleep(delay).await; + let elapsed = start_time.elapsed(); + if elapsed >= self.max_dns_wait { + warn!( + "DNS propagation timeout after {elapsed:?}, max wait time is {max:?}. proceeding anyway as ACME server may have different DNS view", + max = self.max_dns_wait + ); + break; + } + let dns_resolver = AsyncResolver::tokio_from_system_conf().context("failed to create dns resolver")?; @@ -374,6 +398,8 @@ impl AcmeClient { debug!( tries, domain = &challenge.acme_domain, + elapsed = ?elapsed, + max_wait = ?self.max_dns_wait, "challenge not found, waiting for {delay:?}" ); unsettled_challenges.push(challenge); diff --git a/certbot/src/acme_client/tests.rs b/certbot/src/acme_client/tests.rs index 54eefac0..d77504a6 100644 --- a/certbot/src/acme_client/tests.rs +++ b/certbot/src/acme_client/tests.rs @@ -13,7 +13,7 @@ async fn new_acme_client() -> Result { ); let credentials = std::env::var("LETSENCRYPT_CREDENTIAL").expect("LETSENCRYPT_CREDENTIAL not set"); - AcmeClient::load(dns01_client, &credentials).await + AcmeClient::load(dns01_client, &credentials, Duration::from_secs(300)).await } #[tokio::test] diff --git a/certbot/src/bot.rs b/certbot/src/bot.rs index 7df25499..1906bdcd 100644 --- a/certbot/src/bot.rs +++ b/certbot/src/bot.rs @@ -37,6 +37,7 @@ pub struct CertBotConfig { renew_timeout: Duration, renew_expires_in: Duration, renewed_hook: Option, + max_dns_wait: Duration, } impl CertBotConfig { @@ -55,7 +56,7 @@ async fn create_new_account( dns01_client: Dns01Client, ) -> Result { info!("creating new ACME account"); - let client = AcmeClient::new_account(&config.acme_url, dns01_client) + let client = AcmeClient::new_account(&config.acme_url, dns01_client, config.max_dns_wait) .await .context("failed to create new account")?; let credentials = client @@ -82,7 +83,7 @@ impl CertBot { let acme_client = match fs::read_to_string(&config.credentials_file) { Ok(credentials) => { if acme_matches(&credentials, &config.acme_url) { - AcmeClient::load(dns01_client, &credentials).await? + AcmeClient::load(dns01_client, &credentials, config.max_dns_wait).await? } else { create_new_account(&config, dns01_client).await? } diff --git a/certbot/src/bot/tests.rs b/certbot/src/bot/tests.rs index e7c65e6d..03e4ad9d 100644 --- a/certbot/src/bot/tests.rs +++ b/certbot/src/bot/tests.rs @@ -25,6 +25,7 @@ async fn new_certbot() -> Result { .renew_interval(Duration::from_secs(30)) .renew_timeout(Duration::from_secs(120)) .renew_expires_in(Duration::from_secs(7772187)) + .max_dns_wait(Duration::from_secs(300)) .auto_set_caa(false) .build(); config.build_bot().await diff --git a/gateway/dstack-app/builder/entrypoint.sh b/gateway/dstack-app/builder/entrypoint.sh index 7696b95d..862db9a1 100755 --- a/gateway/dstack-app/builder/entrypoint.sh +++ b/gateway/dstack-app/builder/entrypoint.sh @@ -119,6 +119,7 @@ domain = "*.$SRV_DOMAIN" renew_interval = "1h" renew_before_expiration = "10d" renew_timeout = "5m" +max_dns_wait = "${CERTBOT_MAX_DNS_WAIT:-5m}" [core.wg] public_key = "$PUBLIC_KEY" diff --git a/gateway/dstack-app/deploy-to-vmm.sh b/gateway/dstack-app/deploy-to-vmm.sh index 9262efff..46925cfe 100755 --- a/gateway/dstack-app/deploy-to-vmm.sh +++ b/gateway/dstack-app/deploy-to-vmm.sh @@ -84,6 +84,8 @@ GATEWAY_SERVING_ADDR=0.0.0.0:9204 GUEST_AGENT_ADDR=127.0.0.1:9206 WG_ADDR=0.0.0.0:9202 +CERTBOT_MAX_DNS_WAIT=5m + # The token used to launch the App APP_LAUNCH_TOKEN=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) @@ -143,6 +145,7 @@ BOOTNODE_URL=$BOOTNODE_URL SUBNET_INDEX=$SUBNET_INDEX APP_LAUNCH_TOKEN=$APP_LAUNCH_TOKEN RPC_DOMAIN=$RPC_DOMAIN +CERTBOT_MAX_DNS_WAIT=$CERTBOT_MAX_DNS_WAIT EOF if [ -n "$APP_COMPOSE_FILE" ]; then diff --git a/gateway/gateway.toml b/gateway/gateway.toml index a89ff348..4445a138 100644 --- a/gateway/gateway.toml +++ b/gateway/gateway.toml @@ -38,6 +38,7 @@ domain = "*.example.com" renew_interval = "1h" renew_before_expiration = "10d" renew_timeout = "120s" +max_dns_wait = "5m" [core.wg] public_key = "" diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 3a3d88db..03d2b125 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -208,6 +208,9 @@ pub struct CertbotConfig { /// Renew timeout #[serde(with = "serde_duration")] pub renew_timeout: Duration, + /// Maximum time to wait for DNS propagation + #[serde(with = "serde_duration")] + pub max_dns_wait: Duration, } impl CertbotConfig { @@ -227,6 +230,7 @@ impl CertbotConfig { .renew_timeout(self.renew_timeout) .renew_expires_in(self.renew_before_expiration) .auto_set_caa(self.auto_set_caa) + .max_dns_wait(self.max_dns_wait) .build() }