dwara_core/mesh/
sidecar.rs

1//! Sidecar traffic interception (DW-107).
2//!
3//! dwara runs as a sidecar in each pod. An init container (or the
4//! sidecar bootstrap) configures the network so all traffic to and from
5//! the local application flows through the sidecar:
6//!
7//! - INBOUND: traffic destined for the local app is redirected to the
8//!   sidecar's inbound listener. The sidecar terminates mTLS (verifying
9//!   the peer's SPIFFE SVID against the trust bundle), applies policies,
10//!   then forwards the plaintext request to the local app over
11//!   loopback.
12//! - OUTBOUND: traffic the local app sends to a remote service is
13//!   redirected to the sidecar's outbound listener. The sidecar applies
14//!   policies, wraps the request in mTLS (presenting the local
15//!   workload's SVID), then dials the remote service's sidecar.
16//!
17//! # Redirect modes
18//!
19//! Two redirect modes are scaffolded:
20//!
21//! - [`SidecarRedirectMode::Iptables`]: the init container installs
22//!   iptables REDIRECT rules that send traffic to the inbound/outbound
23//!   listener ports. This is the default and the most common mode
24//!   (used by Istio/Linkerd-style sidecars).
25//! - [`SidecarRedirectMode::Tproxy`]: TPROXY (transparent proxy) mode
26//!   uses iptables TPROXY + IP_TRANSPARENT so the sidecar receives the
27//!   original destination address (no REDIRECT port rewrite), enabling
28//!   the sidecar to know the intended upstream without parsing the
29//!   SO_ORIGINAL_DST socket option.
30//!
31//! # Stubbed
32//!
33//! The actual iptables/TPROXY rule installation is STUBBED. The
34//! [`SidecarController`] records the configured inbound/outbound
35//! listeners and documents the redirect setup, but does NOT install
36//! iptables rules or open sockets. The sidecar bootstrap (the init
37//! container that runs as root and configures the network namespace)
38//! would land here when production-ready. Today the controller is the
39//! compile-time scaffold: it validates the configuration shape and
40//! exposes the listener configuration the runtime would bind.
41
42use crate::config::mesh::MeshSidecarConfig;
43
44/// The redirect mode the sidecar bootstrap uses to capture traffic.
45///
46/// [`SidecarRedirectMode::Iptables`] is the default (iptables REDIRECT
47/// rules); [`SidecarRedirectMode::Tproxy`] uses TPROXY for transparent
48/// interception with original-destination preservation.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum SidecarRedirectMode {
51    /// iptables REDIRECT: traffic is rewritten to the sidecar's
52    /// inbound/outbound listener port. The sidecar recovers the
53    /// original destination via `SO_ORIGINAL_DST`.
54    Iptables,
55    /// TPROXY (transparent proxy): the sidecar receives the connection
56    /// with the original destination address preserved (no port
57    /// rewrite), via `IP_TRANSPARENT` + iptables TPROXY.
58    Tproxy,
59}
60
61impl SidecarRedirectMode {
62    /// The lowercase wire name used in config and logs.
63    pub fn as_str(self) -> &'static str {
64        match self {
65            SidecarRedirectMode::Iptables => "iptables",
66            SidecarRedirectMode::Tproxy => "tproxy",
67        }
68    }
69
70    /// Parse a wire-name string into a redirect mode. Returns None for
71    /// an unknown name (the config parser rejects unknown variants
72    /// before this is reached; this is the runtime helper).
73    pub fn parse(s: &str) -> Option<Self> {
74        match s {
75            "iptables" => Some(SidecarRedirectMode::Iptables),
76            "tproxy" => Some(SidecarRedirectMode::Tproxy),
77            _ => None,
78        }
79    }
80}
81
82impl std::fmt::Display for SidecarRedirectMode {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.write_str(self.as_str())
85    }
86}
87
88/// The direction of traffic a sidecar listener handles.
89///
90/// [`SidecarMode::Inbound`] intercepts traffic TO the local app
91/// (terminate mTLS, apply policies, forward to loopback).
92/// [`SidecarMode::Outbound`] intercepts traffic FROM the local app to
93/// remote services (apply policies, wrap in mTLS, dial the remote
94/// sidecar).
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum SidecarMode {
97    /// Intercept traffic destined for the local application:
98    /// terminate the peer's mTLS, verify the peer's SPIFFE SVID, apply
99    /// policies, then forward the plaintext request to the local app
100    /// over loopback.
101    Inbound,
102    /// Intercept traffic the local application sends to a remote
103    /// service: apply policies, wrap the request in mTLS using the
104    /// local workload's SVID, then dial the remote service's sidecar.
105    Outbound,
106}
107
108impl SidecarMode {
109    /// The lowercase wire name used in metrics labels and logs.
110    pub fn as_str(self) -> &'static str {
111        match self {
112            SidecarMode::Inbound => "inbound",
113            SidecarMode::Outbound => "outbound",
114        }
115    }
116}
117
118impl std::fmt::Display for SidecarMode {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.write_str(self.as_str())
121    }
122}
123
124/// The resolved sidecar configuration: the listener ports and redirect
125/// mode the controller binds. Built from the config schema
126/// ([`MeshSidecarConfig`]) at compile time.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SidecarConfig {
129    /// Whether the sidecar is enabled.
130    pub enabled: bool,
131    /// The inbound listener port (traffic to the local app). Must be
132    /// > 0 and distinct from the outbound port.
133    pub inbound_port: u16,
134    /// The outbound listener port (traffic from the local app to
135    /// remote services). Must be > 0 and distinct from the inbound
136    /// port.
137    pub outbound_port: u16,
138    /// The redirect mode (iptables REDIRECT or TPROXY).
139    pub redirect_mode: SidecarRedirectMode,
140}
141
142impl SidecarConfig {
143    /// Build a [`SidecarConfig`] from the config schema. The redirect
144    /// mode string is parsed here; an unknown value yields the default
145    /// (iptables) -- the config parser rejects unknown variants before
146    /// this is reached, so this is a defensive fallback. `enabled` is
147    /// inherited from the parent `MeshConfig.enabled` (the sidecar
148    /// block itself has no separate enabled flag).
149    pub fn from_config(cfg: &MeshSidecarConfig, enabled: bool) -> Self {
150        SidecarConfig {
151            enabled,
152            inbound_port: cfg.inbound_port,
153            outbound_port: cfg.outbound_port,
154            redirect_mode: SidecarRedirectMode::parse(&cfg.redirect_mode)
155                .unwrap_or(SidecarRedirectMode::Iptables),
156        }
157    }
158}
159
160/// The sidecar controller: configures the inbound and outbound
161/// listeners and (when production-ready) the iptables/TPROXY redirect
162/// bootstrap.
163///
164/// # Stubbed
165///
166/// The controller is a SCAFFOLD today. It records the resolved
167/// [`SidecarConfig`] and exposes the listener configuration the runtime
168/// would bind, but does NOT:
169///
170/// - install iptables REDIRECT/TPROXY rules (the init-container
171///   bootstrap that runs as root and configures the pod's network
172///   namespace would land here when production-ready),
173/// - open the inbound/outbound listener sockets,
174/// - terminate or wrap mTLS (the mTLS wiring lives in `security::tls`,
175///   which would consume the SVID cert/key and trust bundle from
176///   [`crate::mesh::spiffe::SpiffeClient`]).
177///
178/// The controller is the compile-time seam: it validates the
179/// configuration shape and documents the runtime contract so the
180/// production wiring lands here without touching config, validation, or
181/// metrics.
182#[derive(Debug, Clone)]
183pub struct SidecarController {
184    config: SidecarConfig,
185}
186
187impl SidecarController {
188    /// Build a controller from the resolved sidecar config.
189    pub fn new(config: SidecarConfig) -> Self {
190        SidecarController { config }
191    }
192
193    /// The resolved sidecar configuration.
194    pub fn config(&self) -> &SidecarConfig {
195        &self.config
196    }
197
198    /// The inbound listener configuration: the port the sidecar binds
199    /// to receive traffic destined for the local app, and the redirect
200    /// mode used to capture it. Inbound terminates mTLS, applies
201    /// policies, and forwards to the local app over loopback.
202    pub fn inbound_listener(&self) -> (SidecarMode, u16, SidecarRedirectMode) {
203        (
204            SidecarMode::Inbound,
205            self.config.inbound_port,
206            self.config.redirect_mode,
207        )
208    }
209
210    /// The outbound listener configuration: the port the sidecar binds
211    /// to receive traffic the local app sends to remote services, and
212    /// the redirect mode used to capture it. Outbound applies policies,
213    /// wraps the request in mTLS, and dials the remote sidecar.
214    pub fn outbound_listener(&self) -> (SidecarMode, u16, SidecarRedirectMode) {
215        (
216            SidecarMode::Outbound,
217            self.config.outbound_port,
218            self.config.redirect_mode,
219        )
220    }
221
222    /// Install the iptables/TPROXY redirect rules that capture traffic
223    /// into the inbound and outbound listeners.
224    ///
225    /// # Stubbed
226    ///
227    /// This is a documented no-op today. The sidecar bootstrap (the
228    /// init container that runs as root and configures the pod's
229    /// network namespace with iptables REDIRECT or TPROXY rules) would
230    /// land here when production-ready. Today the controller records
231    /// the configured redirect mode and logs that the bootstrap is
232    /// stubbed, so an operator who builds with `--features mesh` sees
233    /// the configuration is accepted and the redirect is scaffolded.
234    pub fn install_redirects(&self) {
235        tracing::info!(
236            code = "mesh_sidecar_redirect_stubbed",
237            mode = %self.config.redirect_mode,
238            inbound_port = self.config.inbound_port,
239            outbound_port = self.config.outbound_port,
240            "the sidecar redirect bootstrap is stubbed (DW-107): the iptables/TPROXY rule \
241             installation would land here when production-ready. The controller records the \
242             configured listeners; no redirect rules are installed."
243        );
244    }
245}