acme_proxy_server/sockets.rs
1//! The three listeners' sockets: binding them at startup, planning a rebind on
2//! a reload, and announcing one that has come up.
3
4use std::sync::Arc;
5use std::time::Duration;
6
7use tokio::net::TcpListener;
8use tracing::{error, info, warn};
9
10use acme_proxy_core::config::Config;
11use acme_proxy_store::db::Database;
12
13use super::supervisor::Cells;
14
15/// Validates the admin configuration and binds its socket when it is enabled.
16///
17/// Shared by [`run`](super::run) and [`serve_on`](super::serve_on) rather than
18/// living in one of them: both need it, and the validation must happen **before
19/// anything binds**, so a misconfigured panel cannot take the ACME listener
20/// down with it halfway through startup.
21pub(super) async fn bind_admin(config: &Arc<Config>) -> anyhow::Result<Option<TcpListener>> {
22 acme_proxy_admin::webadmin::check_config(config).inspect_err(|error| {
23 error!(event = "admin_config_invalid", outcome = "failure", error = %error);
24 })?;
25
26 match config.admin.enabled {
27 false => Ok(None),
28 true => Ok(Some(
29 TcpListener::bind(&config.admin.bind_address)
30 .await
31 .inspect_err(|error| {
32 error!(event = "admin_socket_bind_failed",
33 outcome = "failure",
34 bind_address = %config.admin.bind_address,
35 error = %error);
36 })?,
37 )),
38 }
39}
40
41/// Refuses a `[metrics]` bind address that collides with another listener's.
42///
43/// Pure, so a reload runs the same check before rebinding anything — the twin of
44/// [`acme_proxy_admin::webadmin::check_config`], and beside it in
45/// [`prepare_reload`](crate::generation::prepare_reload) for the same reason: a listener configuration that would not start must not be one a
46/// running server can be reloaded into.
47///
48/// Three listeners, so the check is pairwise. `webadmin` already refuses
49/// admin-versus-server; these are the two pairs it cannot see. Checked even when
50/// `[admin]` is off, since enabling the panel later must not be what surfaces a
51/// latent conflict.
52///
53/// # Errors
54///
55/// Names the other key when the two addresses are equal.
56pub fn check_metrics_config(config: &Config) -> anyhow::Result<()> {
57 if !config.metrics.enabled {
58 return Ok(());
59 }
60
61 let bind = &config.metrics.bind_address;
62 for (name, other) in [
63 ("server.bind_address", &config.server.bind_address),
64 ("admin.bind_address", &config.admin.bind_address),
65 ] {
66 if bind == other && (name != "admin.bind_address" || config.admin.enabled) {
67 error!(event = "metrics_config_invalid",
68 outcome = "failure",
69 bind_address = %bind);
70 anyhow::bail!(
71 "metrics.bind_address and {name} are both `{bind}`: the metrics endpoint is a \
72 separate listener and cannot share a socket (give it its own port)"
73 );
74 }
75 }
76 Ok(())
77}
78
79/// Binds the metrics socket when `[metrics]` is on, refusing a collision first.
80///
81/// The twin of [`bind_admin`], and the same shape for the same reason: a socket
82/// that cannot be bound must stop startup rather than leave the process running
83/// with one of its three listeners silently missing.
84///
85/// Deliberately **no loopback check**. `webadmin::check_config` refuses a
86/// non-loopback admin bind without TLS because that listener's cookie is always
87/// `Secure`, which a browser will not store over plain HTTP, so the failure
88/// would be invisible. Nothing here has a cookie: a metrics port reachable from
89/// a Prometheus host on another machine is the intended deployment, and the
90/// firewall is what bounds it.
91pub(super) async fn bind_metrics(config: &Arc<Config>) -> anyhow::Result<Option<TcpListener>> {
92 check_metrics_config(config)?;
93 if !config.metrics.enabled {
94 return Ok(None);
95 }
96
97 let bind = &config.metrics.bind_address;
98 let listener = TcpListener::bind(bind).await.inspect_err(|error| {
99 error!(event = "metrics_socket_bind_failed",
100 outcome = "failure",
101 bind_address = %bind,
102 error = %error);
103 })?;
104 Ok(Some(listener))
105}
106
107/// The three sockets a process may be handed, already bound.
108///
109/// A struct rather than three positional `Option<TcpListener>` parameters, the
110/// `ProfileParts` and `tests/reload.rs::Sockets` convention: they are the same
111/// shape, so a caller swapping two would still compile, in a function whose
112/// whole subject is which socket answers what.
113///
114/// Every field is optional because a role this process does not run holds no
115/// socket — and `admin` and `metrics` were already optional for their own
116/// `enabled` keys.
117#[derive(Default)]
118pub struct Sockets {
119 pub acme: Option<TcpListener>,
120 pub admin: Option<TcpListener>,
121 pub metrics: Option<TcpListener>,
122}
123
124/// One of the three sockets this process may hold.
125///
126/// An enum rather than the `&'static str` the log field wants, so the reload
127/// path's per-role handling is exhaustive: a fourth listener would be a compile
128/// error at every point that has to decide something about one, which is exactly
129/// how the third arrived with `bind_metrics` and `check_metrics_config` in
130/// place and nothing else remembering it existed.
131#[derive(Clone, Copy, PartialEq, Eq)]
132pub(super) enum Role {
133 Acme,
134 Admin,
135 Metrics,
136}
137
138impl Role {
139 /// The `listener` field every log line about this socket carries.
140 pub(super) fn label(self) -> &'static str {
141 match self {
142 Self::Acme => "acme",
143 Self::Admin => "admin",
144 Self::Metrics => "metrics",
145 }
146 }
147
148 /// The key an operator edits to move this socket, for a refusal to name.
149 fn bind_key(self) -> &'static str {
150 match self {
151 Self::Acme => "server.bind_address",
152 Self::Admin => "admin.bind_address",
153 Self::Metrics => "metrics.bind_address",
154 }
155 }
156}
157
158/// What a reload does to one role's socket.
159///
160/// Built while a failure can still refuse the whole reload, applied once nothing
161/// can fail — the same build-then-publish split every other part of a generation
162/// makes, applied to the one resource that cannot simply be constructed twice.
163pub(super) enum SocketPlan {
164 /// The role's address and enablement are both unchanged. Note this is
165 /// decided from the *configuration*, never from the address actually bound:
166 /// a caller supplying its own socket (every test that binds `127.0.0.1:0`)
167 /// is entitled to one that does not match the file, and rebinding it out
168 /// from under them would be this feature breaking its own callers.
169 Keep,
170 /// Serve this newly bound socket: the role was switched on, or its address
171 /// moved. Connections already established are untouched — hyper owns those,
172 /// and only the socket beneath them changes.
173 Serve(TcpListener),
174 /// Release the socket: the role was switched off.
175 Close,
176}
177
178/// The three roles' socket plans, and what to say about them afterwards.
179pub(super) struct SocketPlans {
180 acme: SocketPlan,
181 admin: SocketPlan,
182 metrics: SocketPlan,
183 /// The resolved address of each freshly bound socket, so the announcement
184 /// after the swap names where the listener actually landed.
185 pub(super) bound: Vec<(Role, String)>,
186}
187
188impl SocketPlans {
189 /// The roles whose socket this reload moved, for [`ReloadReport`] — which is
190 /// what a test waits on and what an operator greps.
191 ///
192 /// [`ReloadReport`]: crate::reload::ReloadReport
193 pub(super) fn rebound(&self) -> Vec<&'static str> {
194 self.bound.iter().map(|(role, _)| role.label()).collect()
195 }
196
197 /// Hands each socket to its accept loop. Synchronous and infallible, so it
198 /// sits inside the publishing run beside the routers.
199 pub(super) fn publish(self, cells: &Cells) {
200 for (role, plan, handle) in [
201 (Role::Acme, self.acme, &cells.acme),
202 (Role::Admin, self.admin, &cells.admin),
203 (Role::Metrics, self.metrics, &cells.metrics),
204 ] {
205 match plan {
206 SocketPlan::Keep => {}
207 SocketPlan::Serve(listener) => handle.serve(listener),
208 SocketPlan::Close => {
209 handle.close();
210 info!(
211 event = "server_listener_stopped",
212 outcome = "success",
213 listener = role.label(),
214 "switched off by a configuration reload: the socket is released \
215 and nothing new is accepted on it"
216 );
217 }
218 }
219 }
220 }
221}
222
223/// Decides, and performs, every bind this reload needs.
224///
225/// The ordering rule the whole reload path rests on, applied to sockets: bind
226/// first, so a bad address refuses the reload rather than having already
227/// dropped the live one. Two addresses that differ as strings but collide in
228/// the kernel — `[::]:3000` against `0.0.0.0:3000` — make that bind fail with
229/// `EADDRINUSE`, which is the safe direction: the running socket is still
230/// serving and the refusal names the key.
231///
232/// A `tls.enabled` flip does not appear here at all. The mode is read per
233/// connection (see [`acme_proxy_net::listener`]), so turning TLS on or off keeps the
234/// socket exactly where it is — which is what makes the one case a bind-first
235/// scheme could not serve, an unchanged address, not a case.
236pub(super) fn plan_sockets(
237 roles: super::RoleSet,
238 applied: &Config,
239 proposed: &Config,
240) -> Result<SocketPlans, crate::reload::ReloadError> {
241 let mut bound = Vec::new();
242 let mut plan = |role: Role,
243 was: Option<&str>,
244 now: Option<&str>|
245 -> Result<SocketPlan, crate::reload::ReloadError> {
246 match (was, now) {
247 (None, None) => Ok(SocketPlan::Keep),
248 (Some(_), None) => Ok(SocketPlan::Close),
249 (Some(was), Some(now)) if was == now => Ok(SocketPlan::Keep),
250 (_, Some(now)) => {
251 let listener = acme_proxy_net::listener::bind_blocking(now).map_err(|error| {
252 error!(event = "server_socket_bind_failed",
253 outcome = "failure",
254 listener = role.label(),
255 bind_address = %now,
256 error = %error);
257 crate::reload::ReloadError::Build(format!(
258 "`{}` is `{now}`, which cannot be bound: {error}",
259 role.bind_key()
260 ))
261 })?;
262 bound.push((role, bound_address(Some(&listener), now)));
263 Ok(SocketPlan::Serve(listener))
264 }
265 }
266 };
267
268 // A role this process does not run holds no socket, in either generation,
269 // so it plans `Keep` and nothing is bound for it. `--role` is a command-line
270 // flag and cannot move under a `SIGHUP`, which is what makes comparing the
271 // same set on both sides correct rather than a simplification.
272 let acme_enabled = roles.has(super::ProcessRole::Acme);
273 let admin_enabled =
274 |config: &Config| roles.has(super::ProcessRole::Admin) && config.admin.enabled;
275
276 // Within the roles this process runs, the ACME listener is still never
277 // switched off by a reload — there is no `server.enabled`, and a CA serving
278 // no ACME would be a process with nothing to do.
279 let acme = plan(
280 Role::Acme,
281 acme_enabled.then_some(applied.server.bind_address.as_str()),
282 acme_enabled.then_some(proposed.server.bind_address.as_str()),
283 )?;
284 let admin = plan(
285 Role::Admin,
286 admin_enabled(applied).then_some(applied.admin.bind_address.as_str()),
287 admin_enabled(proposed).then_some(proposed.admin.bind_address.as_str()),
288 )?;
289 let metrics = plan(
290 Role::Metrics,
291 applied
292 .metrics
293 .enabled
294 .then_some(applied.metrics.bind_address.as_str()),
295 proposed
296 .metrics
297 .enabled
298 .then_some(proposed.metrics.bind_address.as_str()),
299 )?;
300
301 Ok(SocketPlans {
302 acme,
303 admin,
304 metrics,
305 bound,
306 })
307}
308
309/// Says the panel is up, and warns about the two states that make it useless.
310///
311/// Run whenever the admin listener **opens** — at startup, and again on a
312/// reload that turns `admin.enabled` on. Separate from the serving path since
313/// that no longer starts or stops per role: the socket is what comes and goes,
314/// and this is what an operator needs told when it does.
315pub(super) async fn announce_admin_listener(
316 config: &Arc<Config>,
317 database: &Arc<Database>,
318 bound: &str,
319) {
320 // A listener nobody holds an account for is a running service with no way
321 // in; say so once, naming the command that fixes it.
322 if acme_proxy_store::admin_user::AdminUser::list_all(database)
323 .await
324 .is_ok_and(|users| users.is_empty())
325 {
326 warn!(
327 event = "admin_no_users",
328 outcome = "advisory",
329 "the web admin is enabled but has no operators: create one with \
330 `acme-proxy admin user create <username>`"
331 );
332 }
333
334 // Repeated on every start while it holds, the `challenge_validation_bypassed`
335 // treatment: these operators can still sign in, they are simply made to
336 // enrol before their session becomes usable, and that stays worth seeing
337 // for exactly as long as it is true.
338 if config.admin.require_mfa
339 && let Ok(count) =
340 acme_proxy_admin::admin::mfa::operators_without_a_factor(database.clone()).await
341 && count > 0
342 {
343 warn!(
344 event = "admin_mfa_enrolment_pending",
345 outcome = "advisory",
346 count = count,
347 "admin.require_mfa is on and some operators have no second factor: \
348 their next sign-in will require enrolment before the session is usable"
349 );
350 }
351
352 // Same treatment for a configured `[admin.notify]` whose messages have
353 // nowhere to go. An operator's security notifications are addressed to
354 // their own `contact_email`, which is settable only from this host
355 // (`admin user contact`) — so a panel-first deployment can have the whole
356 // section configured, believe it is covered by ASVS V6.3.5/V6.3.7, and be
357 // silently falling back to `notify.email.to` or to nothing.
358 if config.admin.notify.enabled.is_empty() {
359 // Nothing configured: no notifications were promised, so an operator
360 // without an address is not a gap.
361 } else if let Ok(count) =
362 acme_proxy_admin::admin::users::operators_without_a_contact(database.clone()).await
363 && count > 0
364 {
365 warn!(
366 event = "admin_notify_contact_missing",
367 outcome = "advisory",
368 count = count,
369 "[admin.notify] is configured but some operators have no contact address: their \
370 security notifications fall back to notify.email.to, or are dropped if that is \
371 empty too — set one with `acme-proxy admin user contact <username> --contact <address>`"
372 );
373 }
374
375 // Swept once now, then on an interval: sessions outlive a restart, so a
376 // startup-only sweep would leak every one an operator never signed out of.
377 let idle = Duration::from_secs(config.admin.session_idle_timeout_seconds);
378 if let Err(error) = acme_proxy_store::admin_session::AdminSession::cleanup(idle, database).await
379 {
380 error!(event = "admin_session_cleanup_failed", outcome = "failure", error = %error);
381 }
382
383 // The **resolved** address, so a `:0` bind is discoverable.
384 info!(
385 event = "admin_listening",
386 outcome = "success",
387 bind_address = %bound,
388 protocol = if config.admin.tls.enabled { "https" } else { "http" },
389 base_url = %config.admin.base_url
390 );
391}
392
393/// The metrics listener's own one-line announcement, and its standing warning.
394pub(super) fn announce_metrics_listener(bound: &str) {
395 info!(
396 event = "metrics_listening",
397 outcome = "success",
398 bind_address = %bound,
399 "unauthenticated by design: the port is the boundary, so firewall it"
400 );
401}
402
403/// The address a socket ended up on, falling back to what was configured.
404///
405/// The two differ for `:0` and for a caller that supplied its own listener; the
406/// resolved one is what an operator needs, and the configured one is all there
407/// is to say when the socket cannot answer.
408pub(super) fn bound_address(listener: Option<&TcpListener>, configured: &str) -> String {
409 listener
410 .and_then(|listener| listener.local_addr().ok())
411 .map_or_else(|| configured.to_string(), |address| address.to_string())
412}