Skip to content

Commit 110bd04

Browse files
Handle ClearHttpsCerts message (#265)
* acme staging config flag * always allow http server shutdown * clear ssl certs & restart server when "no certificates" is selected * pnpm update * fix clippy * cargo fmt * update protos * updaste proto submodule
1 parent 175176c commit 110bd04

File tree

8 files changed

+426
-416
lines changed

8 files changed

+426
-416
lines changed

example-config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ log_level = "info"
1717
rate_limit_per_second = 0
1818
rate_limit_burst = 0
1919
url = "http://localhost:8080"
20+
acme_staging = false

proto

src/acme.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,18 @@ async fn check_domain_resolves(domain: &str) -> anyhow::Result<()> {
123123
pub async fn run_acme_http01(
124124
domain: String,
125125
existing_credentials_json: String,
126+
use_staging: bool,
126127
port80_permit: Option<Port80Permit>,
127128
progress_tx: mpsc::UnboundedSender<AcmeStep>,
128129
) -> anyhow::Result<AcmeCertResult> {
129130
info!("Starting ACME HTTP-01 certificate issuance for domain: {domain}");
130-
info!("Using Let's Encrypt production environment");
131+
let dir_url = if use_staging {
132+
info!("Using Let's Encrypt staging environment");
133+
LetsEncrypt::Staging.url().to_owned()
134+
} else {
135+
info!("Using Let's Encrypt production environment");
136+
LetsEncrypt::Production.url().to_owned()
137+
};
131138

132139
// DNS pre-flight: verify the domain resolves before attempting ACME.
133140
let _ = progress_tx.send(AcmeStep::CheckingDomain);
@@ -140,7 +147,6 @@ pub async fn run_acme_http01(
140147
let (account, credentials) = if existing_credentials_json.is_empty() {
141148
info!("No stored ACME account found; creating a new one with Let's Encrypt");
142149
let builder = Account::builder().context("Failed to create ACME account builder")?;
143-
let dir_url = LetsEncrypt::Production.url().to_owned();
144150
info!("Registering account at ACME directory: {dir_url}");
145151
let (account, credentials) = builder
146152
.create(
@@ -149,7 +155,7 @@ pub async fn run_acme_http01(
149155
contact: &[],
150156
only_return_existing: false,
151157
},
152-
dir_url,
158+
dir_url.clone(),
153159
None,
154160
)
155161
.await

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ pub struct EnvConfig {
8484
/// server is restarted on this port using those certificates.
8585
#[arg(long, env = "DEFGUARD_PROXY_HTTPS_PORT", default_value_t = 443)]
8686
pub https_port: u16,
87+
88+
/// Use Let's Encrypt staging environment for ACME issuance.
89+
#[arg(long, env = "DEFGUARD_PROXY_ACME_STAGING", default_value_t = false)]
90+
pub acme_staging: bool,
8791
}
8892

8993
#[derive(thiserror::Error, Debug)]

src/grpc.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,29 @@ pub(crate) struct ProxyServer {
5656
cert_dir: PathBuf,
5757
reset_tx: broadcast::Sender<()>,
5858
https_cert_tx: broadcast::Sender<(String, String)>,
59+
clear_https_tx: broadcast::Sender<()>,
5960
/// `Some` only when the main HTTP server is bound to port 80.
6061
/// Used to hand off port 80 gracefully during ACME HTTP-01 challenges.
6162
port80_pause_tx: Option<mpsc::Sender<(oneshot::Sender<()>, oneshot::Receiver<()>)>>,
6263
/// Shared log receiver - written by `GrpcLogLayer` for every tracing event.
6364
/// Drained during ACME execution to collect proxy log lines for error reporting.
6465
logs_rx: LogsReceiver,
66+
acme_staging: bool,
6567
}
6668

6769
impl ProxyServer {
68-
#[must_use]
6970
/// Create new `ProxyServer`.
71+
#[must_use]
72+
#[allow(clippy::too_many_arguments)]
7073
pub(crate) fn new(
7174
cookie_key: Arc<RwLock<Option<Key>>>,
7275
cert_dir: PathBuf,
7376
reset_tx: broadcast::Sender<()>,
7477
https_cert_tx: broadcast::Sender<(String, String)>,
78+
clear_https_tx: broadcast::Sender<()>,
7579
port80_pause_tx: Option<mpsc::Sender<(oneshot::Sender<()>, oneshot::Receiver<()>)>>,
7680
logs_rx: LogsReceiver,
81+
acme_staging: bool,
7782
) -> Self {
7883
Self {
7984
cookie_key,
@@ -86,8 +91,10 @@ impl ProxyServer {
8691
cert_dir,
8792
reset_tx,
8893
https_cert_tx,
94+
clear_https_tx,
8995
port80_pause_tx,
9096
logs_rx,
97+
acme_staging,
9198
}
9299
}
93100

@@ -210,8 +217,10 @@ impl Clone for ProxyServer {
210217
cert_dir: self.cert_dir.clone(),
211218
reset_tx: self.reset_tx.clone(),
212219
https_cert_tx: self.https_cert_tx.clone(),
220+
clear_https_tx: self.clear_https_tx.clone(),
213221
port80_pause_tx: self.port80_pause_tx.clone(),
214222
logs_rx: Arc::clone(&self.logs_rx),
223+
acme_staging: self.acme_staging,
215224
}
216225
}
217226
}
@@ -263,6 +272,7 @@ impl proxy_server::Proxy for ProxyServer {
263272
let connected = Arc::clone(&self.connected);
264273
let cookie_key = Arc::clone(&self.cookie_key);
265274
let https_cert_tx = self.https_cert_tx.clone();
275+
let clear_https_tx = self.clear_https_tx.clone();
266276
tokio::spawn(
267277
async move {
268278
let mut stream = request.into_inner();
@@ -288,6 +298,12 @@ impl proxy_server::Proxy for ProxyServer {
288298
);
289299
}
290300
}
301+
core_response::Payload::ClearHttpsCerts(_) => {
302+
info!("Received ClearHttpsCerts from Core");
303+
if let Err(err) = clear_https_tx.send(()) {
304+
error!("Failed to broadcast ClearHttpsCerts: {err}");
305+
}
306+
}
291307
other => {
292308
let maybe_rx = results.write().expect("Failed to acquire lock on results hashmap when processing response").remove(&response.id);
293309
if let Some(rx) = maybe_rx {
@@ -389,6 +405,7 @@ impl proxy_server::Proxy for ProxyServer {
389405

390406
let pause_tx = self.port80_pause_tx.clone();
391407
let logs_rx = Arc::clone(&self.logs_rx);
408+
let acme_staging = self.acme_staging;
392409
tokio::spawn(async move {
393410
// Request a graceful hand-off of port 80 from the main HTTP server if it is bound
394411
// there, so the ACME challenge listener can bind.
@@ -432,8 +449,14 @@ impl proxy_server::Proxy for ProxyServer {
432449
}
433450
});
434451

435-
match acme::run_acme_http01(domain.clone(), existing_credentials, permit, progress_tx)
436-
.await
452+
match acme::run_acme_http01(
453+
domain.clone(),
454+
existing_credentials,
455+
acme_staging,
456+
permit,
457+
progress_tx,
458+
)
459+
.await
437460
{
438461
Ok(acme_result) => {
439462
let cert_event = AcmeIssueEvent {

src/http.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ pub async fn run_server(
316316
let cookie_key = Arc::default();
317317
let (reset_tx, mut reset_rx) = tokio::sync::broadcast::channel(1);
318318
let (https_cert_tx, https_cert_rx) = broadcast::channel::<(String, String)>(1);
319+
let (clear_https_tx, clear_https_rx) = broadcast::channel::<()>(1);
319320

320321
// When the main HTTP server is on port 80, create a channel so the ACME task can request
321322
// a graceful hand-off of port 80 before binding its temporary challenge listener.
@@ -337,8 +338,10 @@ pub async fn run_server(
337338
env_config.cert_dir.clone(),
338339
reset_tx,
339340
https_cert_tx,
341+
clear_https_tx,
340342
port80_pause_tx,
341343
Arc::clone(&logs_rx),
344+
env_config.acme_staging,
342345
);
343346

344347
// Preload existing TLS configuration so /api/v1/info can report "disconnected"
@@ -515,6 +518,7 @@ pub async fn run_server(
515518
);
516519
let mut current_tls: Option<(String, String)> = None;
517520
let mut https_cert_rx = https_cert_rx;
521+
let mut clear_https_rx = clear_https_rx;
518522
let mut port80_pause_rx = port80_pause_rx;
519523

520524
loop {
@@ -570,6 +574,23 @@ pub async fn run_server(
570574
}
571575
}
572576
}
577+
result = clear_https_rx.recv() => {
578+
match result {
579+
Ok(()) => {
580+
info!("Received ClearHttpsCerts, restarting web server without TLS");
581+
current_tls = None;
582+
handle.graceful_shutdown(Some(Duration::from_secs(30)));
583+
let _ = server_task.await;
584+
}
585+
Err(broadcast::error::RecvError::Lagged(_)) => {
586+
warn!("Missed ClearHttpsCerts update; will apply next one");
587+
}
588+
Err(broadcast::error::RecvError::Closed) => {
589+
error!("ClearHttpsCerts channel closed unexpectedly");
590+
break;
591+
}
592+
}
593+
}
573594
// An ACME task needs port 80: gracefully stop the current HTTP server,
574595
// signal the task that port 80 is free, wait until it's done, then let
575596
// the loop restart the server.
@@ -579,7 +600,7 @@ pub async fn run_server(
579600
} else {
580601
std::future::pending().await
581602
}
582-
}, if current_tls.is_none() => {
603+
} => {
583604
info!("ACME task requested port 80; pausing HTTP server");
584605
handle.graceful_shutdown(Some(Duration::from_secs(10)));
585606
let _ = server_task.await;

web/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@
1515
"dependencies": {
1616
"@axa-ch/react-polymorphic-types": "^1.4.1",
1717
"@floating-ui/react": "^0.27.19",
18-
"@inlang/paraglide-js": "^2.15.1",
18+
"@inlang/paraglide-js": "^2.15.2",
1919
"@tanstack/react-devtools": "^0.9.13",
2020
"@tanstack/react-form": "^1.28.6",
21-
"@tanstack/react-query": "^5.95.2",
22-
"@tanstack/react-query-devtools": "^5.95.2",
21+
"@tanstack/react-query": "^5.96.2",
22+
"@tanstack/react-query-devtools": "^5.96.2",
2323
"@tanstack/react-router": "^1.168.10",
2424
"@tanstack/react-router-devtools": "^1.166.11",
2525
"@uidotdev/usehooks": "^2.4.1",
2626
"axios": "^1.14.0",
2727
"change-case": "^5.4.4",
2828
"clsx": "^2.1.1",
2929
"dayjs": "^1.11.20",
30-
"lodash-es": "^4.17.23",
30+
"lodash-es": "^4.18.1",
3131
"motion": "^12.38.0",
3232
"qrcode.react": "^4.2.0",
3333
"qs": "^6.15.0",
@@ -44,21 +44,21 @@
4444
"@tanstack/devtools-vite": "^0.5.5",
4545
"@tanstack/router-plugin": "^1.167.12",
4646
"@types/lodash-es": "^4.17.12",
47-
"@types/node": "^25.5.0",
47+
"@types/node": "^25.5.2",
4848
"@types/qs": "^6.15.0",
4949
"@types/react": "^19.2.14",
5050
"@types/react-dom": "^19.2.3",
5151
"@vitejs/plugin-react-swc": "^4.3.0",
5252
"globals": "^17.4.0",
5353
"prettier": "^3.8.1",
54-
"sass": "^1.98.0",
54+
"sass": "^1.99.0",
5555
"sharp": "^0.34.5",
5656
"stylelint": "^17.6.0",
5757
"stylelint-config-standard-scss": "^17.0.0",
5858
"stylelint-scss": "^7.0.0",
5959
"typescript": "~5.9.3",
60-
"typescript-eslint": "^8.58.0",
61-
"vite": "^7.3.1",
60+
"typescript-eslint": "^8.58.1",
61+
"vite": "^7.3.2",
6262
"vite-plugin-image-optimizer": "^2.0.3"
6363
},
6464
"pnpm": {

0 commit comments

Comments
 (0)