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}