Skip to main content

reddb_server/service_cli/
server_bootstrap.rs

1//! The single server bootstrap path (issue #2133).
2//!
3//! `red server` / `red replica` used to fan out into eight near-identical
4//! `run_*_server` functions. They collapse here into one shape detection, one
5//! [`TransportSet`] of listener descriptors, and one [`BootedNode`] that binds
6//! and serves them. The per-shape differences that were observable to an
7//! operator stay observable, but as data on the descriptors instead of eight
8//! code paths: which sibling listeners a shape attaches, which listener is
9//! fatal, and when each port opens relative to the DB.
10
11use std::future::Future;
12use std::pin::Pin;
13
14use super::*;
15use crate::transport::{
16    TransportDescriptor, TransportKind, TransportReadiness, TransportRole, TransportSet,
17};
18
19/// The server shapes `red server` can be asked for. The shape decides which
20/// listeners exist, which one is fatal, and the OS thread name — exactly the
21/// split the eight `run_*_server` functions encoded.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum ServerShape {
24    Router,
25    Dual,
26    Grpc,
27    Http,
28    Wire,
29    WireTls,
30    Postgres,
31}
32
33impl ServerShape {
34    fn detect(config: &ServerCommandConfig) -> Option<Self> {
35        if config.router_bind_addr.is_some() {
36            return Some(Self::Router);
37        }
38        match (
39            config.grpc_bind_addr.is_some(),
40            config.http_bind_addr.is_some(),
41        ) {
42            (true, true) => Some(Self::Dual),
43            (true, false) => Some(Self::Grpc),
44            (false, true) => Some(Self::Http),
45            (false, false) if config.wire_bind_addr.is_some() => Some(Self::Wire),
46            (false, false) if config.wire_tls_bind_addr.is_some() => Some(Self::WireTls),
47            (false, false) if config.pg_bind_addr.is_some() => Some(Self::Postgres),
48            (false, false) => None,
49        }
50    }
51
52    const fn thread_name(self) -> &'static str {
53        match self {
54            Self::Router => "red-server-router",
55            Self::Dual => "red-server-dual",
56            Self::Grpc => "red-server-grpc",
57            Self::Http => "red-server-http",
58            Self::Wire => "red-server-wire",
59            Self::WireTls => "red-server-wire-tls",
60            Self::Postgres => "red-server-pg-wire",
61        }
62    }
63}
64
65/// How a failed bind is treated, per the pre-#2133 runner that owned it.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67enum BindPolicy {
68    /// Losing the port ends the boot whether or not the operator named it.
69    Fatal,
70    /// Issue #545: fatal when the operator named the port, degraded when the
71    /// address came from a default.
72    ExplicitFatal,
73    /// Sibling listeners that used to bind inside a detached task, where a
74    /// contested port was logged and the node kept serving.
75    Degrade,
76}
77
78/// Per-transport TLS material resolved before the bind.
79enum PreparedTransport {
80    Plain,
81    HttpTls {
82        server_config: Arc<rustls::ServerConfig>,
83        mtls: bool,
84    },
85    GrpcTls(crate::GrpcTlsOptions),
86    WireTls(crate::wire::WireTlsConfig),
87    Postgres(Option<crate::wire::WireTlsConfig>),
88}
89
90struct BoundTransport {
91    descriptor: TransportDescriptor,
92    /// `None` only for the Unix-socket RedWire descriptor, whose listener
93    /// binds the socket itself.
94    listener: Option<TcpListener>,
95    prepared: PreparedTransport,
96}
97
98/// Everything a transport's serve future needs from the booted node.
99struct ServeContext {
100    config: ServerCommandConfig,
101    runtime: RedDBRuntime,
102    auth_store: Arc<AuthStore>,
103    readiness: TransportReadiness,
104    rt_config: RuntimeConfig,
105    worker_threads: usize,
106    /// Issue #2140: one gRPC server per node, so the plain and TLS gRPC
107    /// listeners share a single `Arc<PreparedRegistry>` and a prepared_id
108    /// minted on one socket resolves on the other. `None` when the node
109    /// carries no gRPC transport.
110    grpc_server: Option<RedDBGrpcServer>,
111}
112
113type TransportFuture = Pin<Box<dyn Future<Output = Result<(), String>> + Send>>;
114
115/// A fully initialized node whose HTTP / gRPC listeners are already bound.
116/// Calling [`BootedNode::run`] opens the remaining listeners and serves them
117/// all on the same runtime.
118pub struct BootedNode {
119    config: ServerCommandConfig,
120    runtime: RedDBRuntime,
121    auth_store: Arc<AuthStore>,
122    tokio_runtime: tokio::runtime::Runtime,
123    /// Listeners bound before the DB opened.
124    front: Vec<BoundTransport>,
125    /// Descriptors the pre-#2133 runners only bound once the DB was up.
126    deferred: Vec<TransportDescriptor>,
127    readiness: TransportReadiness,
128    rt_config: RuntimeConfig,
129    worker_threads: usize,
130    _backup_tasks: Option<BackupTasksHandle>,
131    _telemetry_guard: Option<crate::telemetry::TelemetryGuard>,
132}
133
134impl BootedNode {
135    fn boot(config: ServerCommandConfig) -> Result<Self, String> {
136        let Some(shape) = ServerShape::detect(&config) else {
137            return Err("at least one server bind address must be configured".to_string());
138        };
139        let transport_set = transport_set(&config);
140        warn_plaintext_listeners(&config, &transport_set);
141
142        // Phase 1 — the HTTP and gRPC listeners the pre-#2133 runners bound
143        // before opening the DB, so an explicit port collision fails the boot
144        // before any recovery work starts.
145        let mut readiness = TransportReadiness::default();
146        let mut front = Vec::new();
147        let mut deferred = Vec::new();
148        for descriptor in transport_set.descriptors() {
149            if !matches!(descriptor.kind, TransportKind::Http | TransportKind::Grpc) {
150                deferred.push(descriptor.clone());
151                continue;
152            }
153            let listener = bind_listener_for_startup(
154                &mut readiness,
155                descriptor.kind.as_str(),
156                &descriptor.bind_addr,
157                descriptor.explicit,
158            )?;
159            front.push(BoundTransport {
160                descriptor: descriptor.clone(),
161                listener,
162                prepared: PreparedTransport::Plain,
163            });
164        }
165        resolve_degraded_front_binds(shape, &config, &mut front)?;
166
167        let db_options = config.to_db_options()?;
168        let rt_config = detect_runtime_config();
169        let worker_threads = config.workers.unwrap_or(rt_config.suggested_workers);
170        let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
171            .enable_all()
172            .worker_threads(worker_threads)
173            .thread_stack_size(rt_config.stack_size)
174            .build()
175            .map_err(|err| format!("tokio runtime: {err}"))?;
176        let (runtime, auth_store, telemetry_guard) =
177            build_runtime_and_auth_store(&config, db_options.clone())?;
178        let backup_tasks = spawn_backup_tasks_if_configured(&db_options, &runtime);
179
180        // `RED_ADMIN_BIND` / `RED_METRICS_BIND` serve an HTTP surface, so only
181        // the shapes that already owned one honoured them.
182        if matches!(
183            shape,
184            ServerShape::Router | ServerShape::Dual | ServerShape::Http
185        ) {
186            spawn_admin_metrics_listeners(&runtime, &auth_store);
187        }
188
189        Ok(Self {
190            config,
191            runtime,
192            auth_store,
193            tokio_runtime,
194            front,
195            deferred,
196            readiness,
197            rt_config,
198            worker_threads,
199            _backup_tasks: backup_tasks,
200            _telemetry_guard: telemetry_guard,
201        })
202    }
203
204    /// Open the deferred listeners, attach every transport to the node's tokio
205    /// runtime, and park on the shape's primary transport.
206    pub fn run(mut self) -> Result<(), String> {
207        let signal_runtime = self.runtime.clone();
208        let front = std::mem::take(&mut self.front);
209        let deferred = std::mem::take(&mut self.deferred);
210        let mut readiness = std::mem::take(&mut self.readiness);
211        let mut context = ServeContext {
212            config: self.config.clone(),
213            runtime: self.runtime.clone(),
214            auth_store: self.auth_store.clone(),
215            readiness: TransportReadiness::default(),
216            rt_config: self.rt_config.clone(),
217            worker_threads: self.worker_threads,
218            grpc_server: None,
219        };
220
221        self.tokio_runtime.block_on(async move {
222            spawn_lifecycle_signal_handler(signal_runtime).await;
223
224            // Phase 2 — the listeners the pre-#2133 runners opened only once
225            // the DB was up, so an orchestration probe cannot flip ready off a
226            // SYN backlog while recovery is still running.
227            let mut transports = front;
228            for descriptor in deferred {
229                if let Some(bound) =
230                    bind_deferred_transport(&context.config, &mut readiness, descriptor)?
231                {
232                    transports.push(bound);
233                }
234            }
235
236            // Every listener is accounted for now, so the HTTP surfaces built
237            // below snapshot a complete `/health` transport view.
238            context.readiness = readiness;
239            let shared_grpc = build_shared_grpc_server(&context, &transports);
240            context.grpc_server = shared_grpc;
241            let mut primary: Option<TransportFuture> = None;
242            for bound in transports {
243                let kind = bound.descriptor.kind;
244                let role = bound.descriptor.role;
245                let future = serve_transport(bound, &context)?;
246                match role {
247                    TransportRole::Primary => primary = Some(future),
248                    TransportRole::Secondary => {
249                        tokio::spawn(async move {
250                            if let Err(err) = future.await {
251                                log_listener_exit(kind, &err);
252                            }
253                        });
254                    }
255                }
256            }
257
258            let Some(primary) = primary else {
259                return Err("no server transport started".to_string());
260            };
261            primary.await
262        })
263    }
264}
265
266/// Build the canonical descriptor set for one server command.
267pub fn transport_set(config: &ServerCommandConfig) -> TransportSet {
268    let Some(shape) = ServerShape::detect(config) else {
269        return TransportSet::default();
270    };
271
272    let mut descriptors = Vec::new();
273    match shape {
274        // Issue #933: the router demuxes HTTP, gRPC and RedWire off one port,
275        // so no sibling listener is attached to this shape.
276        ServerShape::Router => push(
277            &mut descriptors,
278            TransportKind::Router,
279            &config.router_bind_addr,
280            config.router_bind_explicit,
281            TransportRole::Primary,
282        ),
283        ServerShape::Dual => {
284            push(
285                &mut descriptors,
286                TransportKind::Http,
287                &config.http_bind_addr,
288                config.http_bind_explicit,
289                TransportRole::Secondary,
290            );
291            push(
292                &mut descriptors,
293                TransportKind::Grpc,
294                &config.grpc_bind_addr,
295                config.grpc_bind_explicit,
296                TransportRole::Primary,
297            );
298            push_wire_family(&mut descriptors, config);
299            push(
300                &mut descriptors,
301                TransportKind::Https,
302                &config.http_tls_bind_addr,
303                true,
304                TransportRole::Secondary,
305            );
306            push(
307                &mut descriptors,
308                TransportKind::GrpcTls,
309                &config.grpc_tls_bind_addr,
310                true,
311                TransportRole::Secondary,
312            );
313        }
314        // The gRPC-only shape never looked at `--http-tls-bind`.
315        ServerShape::Grpc => {
316            push(
317                &mut descriptors,
318                TransportKind::Grpc,
319                &config.grpc_bind_addr,
320                config.grpc_bind_explicit,
321                TransportRole::Primary,
322            );
323            push_wire_family(&mut descriptors, config);
324            push(
325                &mut descriptors,
326                TransportKind::GrpcTls,
327                &config.grpc_tls_bind_addr,
328                true,
329                TransportRole::Secondary,
330            );
331        }
332        // The HTTP-only shape never looked at `--grpc-tls-bind`.
333        ServerShape::Http => {
334            push(
335                &mut descriptors,
336                TransportKind::Http,
337                &config.http_bind_addr,
338                config.http_bind_explicit,
339                TransportRole::Primary,
340            );
341            push_wire_family(&mut descriptors, config);
342            push(
343                &mut descriptors,
344                TransportKind::Https,
345                &config.http_tls_bind_addr,
346                true,
347                TransportRole::Secondary,
348            );
349        }
350        // The wire shapes carried the PG sibling only. `--http-tls-bind` and
351        // `--grpc-tls-bind` were ignored outright, validation included, and
352        // the plaintext / TLS wire flags never coexisted.
353        ServerShape::Wire => {
354            push(
355                &mut descriptors,
356                TransportKind::Wire,
357                &config.wire_bind_addr,
358                config.wire_bind_explicit,
359                TransportRole::Primary,
360            );
361            push(
362                &mut descriptors,
363                TransportKind::Postgres,
364                &config.pg_bind_addr,
365                true,
366                TransportRole::Secondary,
367            );
368        }
369        ServerShape::WireTls => {
370            push(
371                &mut descriptors,
372                TransportKind::WireTls,
373                &config.wire_tls_bind_addr,
374                true,
375                TransportRole::Primary,
376            );
377            push(
378                &mut descriptors,
379                TransportKind::Postgres,
380                &config.pg_bind_addr,
381                true,
382                TransportRole::Secondary,
383            );
384        }
385        ServerShape::Postgres => push(
386            &mut descriptors,
387            TransportKind::Postgres,
388            &config.pg_bind_addr,
389            true,
390            TransportRole::Primary,
391        ),
392    }
393    TransportSet::new(descriptors)
394}
395
396/// Initialize one node and bind its front transports before serving.
397pub fn bootstrap_server(config: ServerCommandConfig) -> Result<BootedNode, String> {
398    BootedNode::boot(config)
399}
400
401/// The only process-level server run path.
402pub fn run_server(config: ServerCommandConfig) -> Result<(), String> {
403    let Some(shape) = ServerShape::detect(&config) else {
404        return Err("at least one server bind address must be configured".to_string());
405    };
406    let handle = thread::Builder::new()
407        .name(shape.thread_name().to_string())
408        .stack_size(8 * 1024 * 1024)
409        .spawn(move || bootstrap_server(config)?.run())
410        .map_err(|err| format!("failed to spawn server thread: {err}"))?;
411
412    match handle.join() {
413        Ok(result) => result,
414        Err(_) => Err("server thread panicked".to_string()),
415    }
416}
417
418fn push(
419    descriptors: &mut Vec<TransportDescriptor>,
420    kind: TransportKind,
421    bind_addr: &Option<String>,
422    explicit: bool,
423    role: TransportRole,
424) {
425    if let Some(bind_addr) = bind_addr.clone() {
426        descriptors.push(TransportDescriptor::new(kind, bind_addr, explicit, role));
427    }
428}
429
430/// The RedWire and PG-Wire listeners every HTTP/gRPC shape attached to its
431/// node alongside its own transport.
432fn push_wire_family(descriptors: &mut Vec<TransportDescriptor>, config: &ServerCommandConfig) {
433    push(
434        descriptors,
435        TransportKind::Wire,
436        &config.wire_bind_addr,
437        config.wire_bind_explicit,
438        TransportRole::Secondary,
439    );
440    push(
441        descriptors,
442        TransportKind::WireTls,
443        &config.wire_tls_bind_addr,
444        true,
445        TransportRole::Secondary,
446    );
447    push(
448        descriptors,
449        TransportKind::Postgres,
450        &config.pg_bind_addr,
451        true,
452        TransportRole::Secondary,
453    );
454}
455
456/// Apply the pre-#2133 rules for an HTTP/gRPC front bind that degraded away: a
457/// shape whose own listener is gone cannot boot, and the dual shape falls back
458/// to parking on HTTP when only gRPC lost its port.
459fn resolve_degraded_front_binds(
460    shape: ServerShape,
461    config: &ServerCommandConfig,
462    front: &mut Vec<BoundTransport>,
463) -> Result<(), String> {
464    let bound = |kind: TransportKind| {
465        front
466            .iter()
467            .any(|transport| transport.descriptor.kind == kind && transport.listener.is_some())
468    };
469    let http_bound = bound(TransportKind::Http);
470    let grpc_bound = bound(TransportKind::Grpc);
471
472    match shape {
473        ServerShape::Http if !http_bound => {
474            return Err(format!(
475                "no HTTP listener started; implicit bind {} failed",
476                config.http_bind_addr.as_deref().unwrap_or_default()
477            ));
478        }
479        ServerShape::Grpc if !grpc_bound => {
480            return Err(format!(
481                "no gRPC listener started; implicit bind {} failed",
482                config.grpc_bind_addr.as_deref().unwrap_or_default()
483            ));
484        }
485        ServerShape::Dual if !http_bound && !grpc_bound => {
486            return Err("no listener started; implicit HTTP and gRPC binds failed".to_string());
487        }
488        ServerShape::Dual if !grpc_bound => {
489            for transport in front.iter_mut() {
490                if transport.descriptor.kind == TransportKind::Http {
491                    transport.descriptor.role = TransportRole::Primary;
492                }
493            }
494        }
495        _ => {}
496    }
497
498    front.retain(|transport| transport.listener.is_some());
499    Ok(())
500}
501
502const fn bind_policy(descriptor: &TransportDescriptor) -> BindPolicy {
503    if matches!(descriptor.role, TransportRole::Primary) {
504        return BindPolicy::Fatal;
505    }
506    match descriptor.kind {
507        // reddb-io/rio-lair#255: an explicit `--wire-tls-bind` must never
508        // degrade into "healthy HTTP, dead TLS".
509        TransportKind::WireTls => BindPolicy::Fatal,
510        TransportKind::Wire => BindPolicy::ExplicitFatal,
511        _ => BindPolicy::Degrade,
512    }
513}
514
515/// Resolve a descriptor's TLS material and open its listener, inside the tokio
516/// runtime and after the DB is up.
517fn bind_deferred_transport(
518    config: &ServerCommandConfig,
519    readiness: &mut TransportReadiness,
520    descriptor: TransportDescriptor,
521) -> Result<Option<BoundTransport>, String> {
522    let transport = descriptor.kind.as_str();
523    let prepared = match descriptor.kind {
524        TransportKind::Https => {
525            let tls = resolve_http_tls_config(config)?;
526            let server_config = crate::server::tls::build_server_config(&tls)
527                .map_err(|err| format!("HTTP TLS: {err}"))?;
528            PreparedTransport::HttpTls {
529                server_config,
530                mtls: tls.client_ca_path.is_some(),
531            }
532        }
533        TransportKind::GrpcTls => match resolve_grpc_tls_options(config) {
534            Ok(options) => PreparedTransport::GrpcTls(options),
535            Err(err) => {
536                let reason = format!("gRPC TLS config error: {err}");
537                readiness.failed(
538                    transport,
539                    &descriptor.bind_addr,
540                    descriptor.explicit,
541                    reason,
542                );
543                tracing::error!(
544                    target: "reddb::security",
545                    transport = "grpc",
546                    err = %err,
547                    "gRPC TLS config error; TLS listener will not start"
548                );
549                return Ok(None);
550            }
551        },
552        TransportKind::WireTls => match resolve_wire_tls_config(config) {
553            Ok(tls) => PreparedTransport::WireTls(tls),
554            Err(err) => {
555                let reason = format!("redwire TLS config error: {err}");
556                readiness.failed(
557                    transport,
558                    &descriptor.bind_addr,
559                    descriptor.explicit,
560                    reason.clone(),
561                );
562                tracing::error!(
563                    transport = "wire-tls",
564                    bind = %descriptor.bind_addr,
565                    error = %err,
566                    "fatal explicit redwire TLS config error"
567                );
568                return Err(format!("explicit {reason}"));
569            }
570        },
571        TransportKind::Postgres => PreparedTransport::Postgres(resolve_pg_wire_tls(config)),
572        _ => PreparedTransport::Plain,
573    };
574
575    // A `unix://` or absolute-path RedWire address is a Unix socket; that
576    // listener binds the socket itself.
577    #[cfg(unix)]
578    if descriptor.kind == TransportKind::Wire
579        && (descriptor.bind_addr.starts_with("unix://") || descriptor.bind_addr.starts_with('/'))
580    {
581        readiness.active(transport, &descriptor.bind_addr, descriptor.explicit);
582        return Ok(Some(BoundTransport {
583            descriptor,
584            listener: None,
585            prepared,
586        }));
587    }
588
589    let listener = match bind_policy(&descriptor) {
590        BindPolicy::Fatal => Some(bind_fatal_listener(readiness, &descriptor)?),
591        BindPolicy::ExplicitFatal => bind_listener_for_startup(
592            readiness,
593            transport,
594            &descriptor.bind_addr,
595            descriptor.explicit,
596        )?,
597        BindPolicy::Degrade => bind_degrading_listener(readiness, &descriptor),
598    };
599    let Some(listener) = listener else {
600        return Ok(None);
601    };
602    Ok(Some(BoundTransport {
603        descriptor,
604        listener: Some(listener),
605        prepared,
606    }))
607}
608
609/// Bind a listener whose loss ends the boot whether or not the operator named
610/// the port — the shape has nothing left to serve without it.
611fn bind_fatal_listener(
612    readiness: &mut TransportReadiness,
613    descriptor: &TransportDescriptor,
614) -> Result<TcpListener, String> {
615    match bind_listener_for_startup(
616        readiness,
617        descriptor.kind.as_str(),
618        &descriptor.bind_addr,
619        descriptor.explicit,
620    ) {
621        Ok(Some(listener)) => Ok(listener),
622        // An implicit default that degraded away: promote the reason the #545
623        // recorder just captured into the fatal error.
624        Ok(None) => Err(readiness
625            .failed
626            .last()
627            .map(|failure| failure.reason.clone())
628            .unwrap_or_else(|| {
629                format!(
630                    "{} listener bind {} failed",
631                    descriptor.kind.as_str(),
632                    descriptor.bind_addr
633                )
634            })),
635        Err(err) => Err(err),
636    }
637}
638
639/// Bind a sibling listener that pre-#2133 opened inside a detached task: a
640/// contested port is logged and recorded, and the node keeps serving.
641fn bind_degrading_listener(
642    readiness: &mut TransportReadiness,
643    descriptor: &TransportDescriptor,
644) -> Option<TcpListener> {
645    let transport = descriptor.kind.as_str();
646    match TcpListener::bind(&descriptor.bind_addr) {
647        Ok(listener) => {
648            readiness.active(transport, &descriptor.bind_addr, descriptor.explicit);
649            Some(listener)
650        }
651        Err(err) => {
652            let reason = format!("{transport} listener bind {}: {err}", descriptor.bind_addr);
653            readiness.failed(
654                transport,
655                &descriptor.bind_addr,
656                descriptor.explicit,
657                reason,
658            );
659            tracing::error!(
660                transport,
661                bind = %descriptor.bind_addr,
662                error = %err,
663                "non-fatal listener bind failure; listener degraded"
664            );
665            None
666        }
667    }
668}
669
670/// Build the node's single gRPC server, shared by its plain and TLS gRPC
671/// listeners (issue #2140). Constructing one server per listener would give
672/// each its own `PreparedRegistry`, so a prepared_id minted on the plain port
673/// would not resolve on the TLS port. Returns `None` when the node carries no
674/// gRPC transport, so non-gRPC shapes build no server at all.
675fn build_shared_grpc_server(
676    context: &ServeContext,
677    transports: &[BoundTransport],
678) -> Option<RedDBGrpcServer> {
679    let bind_addr = transports
680        .iter()
681        .find(|bound| bound.descriptor.kind == TransportKind::Grpc)
682        .or_else(|| {
683            transports
684                .iter()
685                .find(|bound| bound.descriptor.kind == TransportKind::GrpcTls)
686        })
687        .map(|bound| bound.descriptor.bind_addr.clone())?;
688    Some(RedDBGrpcServer::with_options(
689        context.runtime.clone(),
690        GrpcServerOptions {
691            bind_addr,
692            tls: None,
693        },
694        context.auth_store.clone(),
695    ))
696}
697
698/// Build the future that serves one bound transport for the node's lifetime.
699fn serve_transport(
700    bound: BoundTransport,
701    context: &ServeContext,
702) -> Result<TransportFuture, String> {
703    let ServeContext {
704        config,
705        runtime,
706        auth_store,
707        readiness,
708        rt_config,
709        worker_threads,
710        grpc_server,
711    } = context;
712    let bind_addr = bound.descriptor.bind_addr.clone();
713
714    match (bound.descriptor.kind, bound.listener, bound.prepared) {
715        (TransportKind::Router, Some(listener), PreparedTransport::Plain) => {
716            let http_server =
717                build_http_server(runtime.clone(), auth_store.clone(), bind_addr.clone());
718            let http_server = apply_http_limits(http_server, config, runtime);
719            let http_server = apply_ui_bundle(http_server, config)?;
720            let grpc_server = RedDBGrpcServer::with_options(
721                runtime.clone(),
722                GrpcServerOptions {
723                    bind_addr: bind_addr.clone(),
724                    tls: None,
725                },
726                auth_store.clone(),
727            );
728            let wire_runtime = Arc::new(runtime.clone());
729            let cpus = rt_config.available_cpus;
730            let workers = *worker_threads;
731            Ok(Box::pin(async move {
732                tracing::info!(
733                    bind = %bind_addr,
734                    cpus,
735                    workers,
736                    "router bootstrapping"
737                );
738                serve_tcp_router(InProcessRouterConfig {
739                    listener,
740                    bind_addr,
741                    http_server,
742                    grpc_server,
743                    wire_runtime,
744                })
745                .await
746                .map_err(|err| err.to_string())
747            }))
748        }
749        (TransportKind::Http, Some(listener), PreparedTransport::Plain) => {
750            let server = build_http_server_with_transport_readiness(
751                runtime.clone(),
752                auth_store.clone(),
753                bind_addr.clone(),
754                readiness.clone(),
755            );
756            let server = apply_http_limits(server, config, runtime);
757            let server = apply_ui_bundle(server, config)?;
758            Ok(Box::pin(async move {
759                tracing::info!(transport = "http", bind = %bind_addr, "listener online");
760                join_http_thread(server.serve_in_background_on(listener)).await
761            }))
762        }
763        (
764            TransportKind::Https,
765            Some(listener),
766            PreparedTransport::HttpTls {
767                server_config,
768                mtls,
769            },
770        ) => {
771            let server = build_http_server(runtime.clone(), auth_store.clone(), bind_addr.clone());
772            let server = apply_http_limits(server, config, runtime);
773            Ok(Box::pin(async move {
774                tracing::info!(
775                    transport = "https",
776                    bind = %bind_addr,
777                    mtls = %mtls,
778                    "TLS listener online"
779                );
780                join_http_thread(server.serve_tls_in_background_on(listener, server_config)).await
781            }))
782        }
783        (TransportKind::Grpc, Some(listener), PreparedTransport::Plain) => {
784            let server =
785                shared_grpc_server(grpc_server)?.with_listener_options(GrpcServerOptions {
786                    bind_addr: bind_addr.clone(),
787                    tls: None,
788                });
789            let cpus = rt_config.available_cpus;
790            let workers = *worker_threads;
791            Ok(Box::pin(async move {
792                tracing::info!(
793                    transport = "grpc",
794                    bind = %bind_addr,
795                    cpus,
796                    workers,
797                    "listener online"
798                );
799                server
800                    .serve_on(listener)
801                    .await
802                    .map_err(|err| err.to_string())
803            }))
804        }
805        (TransportKind::GrpcTls, Some(listener), PreparedTransport::GrpcTls(tls)) => {
806            // Issue #2140: the same server instance the plain gRPC listener
807            // serves, so both sockets share one `Arc<PreparedRegistry>`.
808            let server =
809                shared_grpc_server(grpc_server)?.with_listener_options(GrpcServerOptions {
810                    bind_addr: bind_addr.clone(),
811                    tls: Some(tls),
812                });
813            Ok(Box::pin(async move {
814                tracing::info!(transport = "grpc+tls", bind = %bind_addr, "listener online");
815                server
816                    .serve_on(listener)
817                    .await
818                    .map_err(|err| err.to_string())
819            }))
820        }
821        (TransportKind::Wire, listener, PreparedTransport::Plain) => {
822            let wire_runtime = Arc::new(runtime.clone());
823            Ok(Box::pin(async move {
824                // `start_redwire_listener_on` never logged itself online; the
825                // #545 readiness record is this transport's only signal.
826                #[cfg(unix)]
827                if listener.is_none() {
828                    return crate::wire::redwire::listener::start_redwire_unix_listener(
829                        &bind_addr,
830                        wire_runtime,
831                    )
832                    .await
833                    .map_err(|err| err.to_string());
834                }
835                let listener = into_tokio_listener(
836                    listener.expect("TCP wire descriptor carries a bound listener"),
837                )?;
838                crate::wire::redwire::listener::start_redwire_listener_on(listener, wire_runtime)
839                    .await
840                    .map_err(|err| err.to_string())
841            }))
842        }
843        (TransportKind::WireTls, Some(listener), PreparedTransport::WireTls(tls)) => {
844            let wire_runtime = Arc::new(runtime.clone());
845            Ok(Box::pin(async move {
846                let listener = into_tokio_listener(listener)?;
847                // `start_redwire_tls_listener_on` does not log for itself, and
848                // issues #1588 / #545 pin this exact line as the wire-TLS
849                // readiness probe.
850                tracing::info!(transport = "redwire+tls", bind = %bind_addr, "listener online");
851                crate::wire::start_redwire_tls_listener_on(listener, wire_runtime, &tls)
852                    .await
853                    .map_err(|err| err.to_string())
854            }))
855        }
856        (TransportKind::Postgres, Some(listener), PreparedTransport::Postgres(tls)) => {
857            let pg_runtime = Arc::new(runtime.clone());
858            Ok(Box::pin(async move {
859                let listener = into_tokio_listener(listener)?;
860                let pg_config = crate::wire::PgWireConfig {
861                    bind_addr,
862                    tls,
863                    ..Default::default()
864                };
865                crate::wire::start_pg_wire_listener_on(listener, pg_config, pg_runtime)
866                    .await
867                    .map_err(|err| err.to_string())
868            }))
869        }
870        _ => Err("transport descriptor preparation mismatch".to_string()),
871    }
872}
873
874/// Clone the node's shared gRPC server for one listener. Absent only if a gRPC
875/// transport reached [`serve_transport`] without one being built.
876fn shared_grpc_server(server: &Option<RedDBGrpcServer>) -> Result<RedDBGrpcServer, String> {
877    server
878        .clone()
879        .ok_or_else(|| "gRPC transport served without a bootstrapped gRPC server".to_string())
880}
881
882/// Hand a listener bound during bootstrap over to tokio.
883fn into_tokio_listener(listener: TcpListener) -> Result<tokio::net::TcpListener, String> {
884    listener
885        .set_nonblocking(true)
886        .map_err(|err| err.to_string())?;
887    tokio::net::TcpListener::from_std(listener).map_err(|err| err.to_string())
888}
889
890/// Park on a background HTTP/HTTPS server thread without holding a tokio
891/// worker.
892async fn join_http_thread(handle: thread::JoinHandle<std::io::Result<()>>) -> Result<(), String> {
893    match tokio::task::spawn_blocking(move || handle.join()).await {
894        Ok(Ok(Ok(()))) => Err("HTTP server exited unexpectedly".to_string()),
895        Ok(Ok(Err(err))) => Err(err.to_string()),
896        Ok(Err(_)) => Err("HTTP server thread panicked".to_string()),
897        Err(join_err) => Err(format!("HTTP server monitor task failed: {join_err}")),
898    }
899}
900
901/// Pre-#2133 wording for a sibling listener whose accept loop exited.
902fn log_listener_exit(kind: TransportKind, err: &str) {
903    match kind {
904        TransportKind::Wire => tracing::error!(err, "redwire listener error"),
905        TransportKind::WireTls => tracing::error!(err, "redwire+tls listener error"),
906        TransportKind::Postgres => tracing::error!(err, "pg wire listener error"),
907        TransportKind::GrpcTls => {
908            tracing::error!(transport = "grpc+tls", err, "gRPC TLS listener error")
909        }
910        _ => tracing::error!(transport = kind.as_str(), err, "listener error"),
911    }
912}
913
914/// Announce every listener that will accept connections from beyond the
915/// host without TLS. Plaintext is still the default, so an operator who
916/// never configured a certificate has never been told that credentials
917/// and data cross the network in clear.
918fn warn_plaintext_listeners(config: &ServerCommandConfig, transports: &TransportSet) {
919    use crate::transport::{is_loopback_bind, TransportKind};
920    let http_tls = config.http_tls_bind_addr.is_some()
921        || (config.http_tls_cert.is_some() && config.http_tls_key.is_some());
922    let grpc_tls = config.grpc_tls_bind_addr.is_some()
923        || (config.grpc_tls_cert.is_some() && config.grpc_tls_key.is_some());
924    let wire_tls = config.wire_tls_bind_addr.is_some()
925        || (config.wire_tls_cert.is_some() && config.wire_tls_key.is_some());
926    for descriptor in transports.descriptors() {
927        let tls_configured = match descriptor.kind {
928            TransportKind::Http => http_tls,
929            TransportKind::Grpc => grpc_tls,
930            TransportKind::Wire | TransportKind::Postgres => wire_tls,
931            TransportKind::Router => http_tls || grpc_tls || wire_tls,
932            TransportKind::Https | TransportKind::GrpcTls | TransportKind::WireTls => true,
933        };
934        if tls_configured || is_loopback_bind(&descriptor.bind_addr) {
935            continue;
936        }
937        let message = format!(
938            "⚠ {} listener on {} is plaintext and reachable beyond this host — \
939             credentials and data cross the network in clear; configure TLS or \
940             bind to loopback behind a TLS-terminating proxy",
941            descriptor.kind.as_str(),
942            descriptor.bind_addr
943        );
944        eprintln!("{message}");
945        tracing::warn!(
946            target: "reddb::security",
947            transport = descriptor.kind.as_str(),
948            bind = %descriptor.bind_addr,
949            "{message}"
950        );
951    }
952}