1use std::future::Future;
12use std::pin::Pin;
13
14use super::*;
15use crate::transport::{
16 TransportDescriptor, TransportKind, TransportReadiness, TransportRole, TransportSet,
17};
18
19#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67enum BindPolicy {
68 Fatal,
70 ExplicitFatal,
73 Degrade,
76}
77
78enum 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 listener: Option<TcpListener>,
95 prepared: PreparedTransport,
96}
97
98struct ServeContext {
100 config: ServerCommandConfig,
101 runtime: RedDBRuntime,
102 auth_store: Arc<AuthStore>,
103 readiness: TransportReadiness,
104 rt_config: RuntimeConfig,
105 worker_threads: usize,
106 grpc_server: Option<RedDBGrpcServer>,
111}
112
113type TransportFuture = Pin<Box<dyn Future<Output = Result<(), String>> + Send>>;
114
115pub struct BootedNode {
119 config: ServerCommandConfig,
120 runtime: RedDBRuntime,
121 auth_store: Arc<AuthStore>,
122 tokio_runtime: tokio::runtime::Runtime,
123 front: Vec<BoundTransport>,
125 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 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 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 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 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 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
266pub 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 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 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 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 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
396pub fn bootstrap_server(config: ServerCommandConfig) -> Result<BootedNode, String> {
398 BootedNode::boot(config)
399}
400
401pub 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
430fn 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
456fn 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 TransportKind::WireTls => BindPolicy::Fatal,
510 TransportKind::Wire => BindPolicy::ExplicitFatal,
511 _ => BindPolicy::Degrade,
512 }
513}
514
515fn 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 #[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
609fn 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 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
639fn 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
670fn 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
698fn 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 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 #[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 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
874fn 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
882fn 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
890async 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
901fn 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
914fn 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}