dwara_core/config/mesh.rs
1//! Service mesh config schema (DW-107).
2//!
3//! The top-level `mesh` block configures service mesh mode: dwara runs
4//! as a sidecar in each pod, intercepting traffic via iptables/TPROXY
5//! redirect, with mTLS identity provided by SPIFFE/SPIRE (X.509 SVIDs).
6//! The schema is always present so configs round-trip; the mesh runtime
7//! is scaffolded and Enterprise-only: validation warns when mesh is
8//! configured without the `ent` feature (or when the scaffolded
9//! runtime is not wired).
10//!
11//! See the `mesh` domain module for the sidecar
12//! controller and SPIFFE client scaffolds.
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17/// The service mesh mode. v1 ships `sidecar` (dwara runs as a sidecar
18/// in each pod). Future modes (node-level, CNI-based) are deferred;
19/// validation rejects any value other than `sidecar`.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
21#[serde(rename_all = "lowercase")]
22pub enum MeshMode {
23 /// Sidecar mode: dwara runs as a sidecar in each pod, intercepting
24 /// traffic via iptables/TPROXY redirect. Inbound terminates mTLS
25 /// and forwards to the local app; outbound wraps in mTLS and dials
26 /// the remote sidecar.
27 Sidecar,
28}
29
30impl MeshMode {
31 /// The lowercase wire name.
32 pub fn as_str(self) -> &'static str {
33 match self {
34 MeshMode::Sidecar => "sidecar",
35 }
36 }
37}
38
39impl std::fmt::Display for MeshMode {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.write_str(self.as_str())
42 }
43}
44
45/// Top-level service mesh config (DW-107, `gateway.mesh`).
46///
47/// When present, the gateway is configured to run as a sidecar in each
48/// pod: an init container configures
49/// iptables/TPROXY redirects so all traffic to and from the local
50/// application flows through the sidecar, which terminates mTLS
51/// (inbound) and wraps mTLS (outbound) using SPIFFE/SPIRE X.509 SVIDs.
52/// The mesh runtime is scaffolded and Enterprise-only: the block is
53/// accepted but inert (validation warns; no sidecar listeners or
54/// SPIFFE client are wired) without the `ent` feature and until the
55/// sidecar bootstrap is production-ready.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
57#[serde(deny_unknown_fields)]
58pub struct MeshConfig {
59 /// Master switch. Default false: the mesh is inert when false
60 /// (the runtime is scaffolded regardless). This lets operators stage
61 /// the config ahead of activating the surface.
62 #[serde(default, skip_serializing_if = "is_false")]
63 pub enabled: bool,
64 /// The mesh mode. v1 ships `sidecar`; validation rejects any other
65 /// value.
66 pub mode: MeshMode,
67 /// Sidecar listener and redirect configuration. Required when
68 /// `mode` is `sidecar`.
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub sidecar: Option<MeshSidecarConfig>,
71 /// SPIFFE/SPIRE identity configuration. Required when the mesh is
72 /// enabled (mTLS identity is mandatory, not optional).
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub spiffe: Option<MeshSpiffeConfig>,
75}
76
77/// Sidecar listener and redirect config (DW-107, `gateway.mesh.sidecar`).
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
79#[serde(deny_unknown_fields)]
80pub struct MeshSidecarConfig {
81 /// The inbound listener port (traffic to the local app). Default
82 /// 15006 (the Istio convention). Must be > 0 and distinct from the
83 /// outbound port.
84 #[serde(
85 default = "default_sidecar_inbound_port",
86 skip_serializing_if = "is_default_sidecar_inbound_port"
87 )]
88 pub inbound_port: u16,
89 /// The outbound listener port (traffic from the local app to
90 /// remote services). Default 15001 (the Istio convention). Must be
91 /// > 0 and distinct from the inbound port.
92 #[serde(
93 default = "default_sidecar_outbound_port",
94 skip_serializing_if = "is_default_sidecar_outbound_port"
95 )]
96 pub outbound_port: u16,
97 /// The redirect mode: `iptables` (REDIRECT rules, the default) or
98 /// `tproxy` (TPROXY with original-destination preservation).
99 /// Validation rejects any value other than `iptables` or `tproxy`.
100 #[serde(
101 default = "default_sidecar_redirect_mode",
102 skip_serializing_if = "is_default_sidecar_redirect_mode"
103 )]
104 pub redirect_mode: String,
105}
106
107fn default_sidecar_inbound_port() -> u16 {
108 15006
109}
110
111fn is_default_sidecar_inbound_port(v: &u16) -> bool {
112 *v == 15006
113}
114
115fn default_sidecar_outbound_port() -> u16 {
116 15001
117}
118
119fn is_default_sidecar_outbound_port(v: &u16) -> bool {
120 *v == 15001
121}
122
123fn default_sidecar_redirect_mode() -> String {
124 "iptables".to_string()
125}
126
127fn is_default_sidecar_redirect_mode(v: &str) -> bool {
128 v == "iptables"
129}
130
131/// SPIFFE/SPIRE identity config (DW-107, `gateway.mesh.spiffe`).
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
133#[serde(deny_unknown_fields)]
134pub struct MeshSpiffeConfig {
135 /// The trust domain this workload belongs to (e.g. `example.org`).
136 /// Must be non-empty. Forms the SPIFFE ID prefix
137 /// (`spiffe://<trust-domain>/...`).
138 pub trust_domain: String,
139 /// The filesystem path of the SPIRE Workload API Unix socket
140 /// (e.g. `/tmp/spire-agent/public/api.sock`). Must be non-empty.
141 /// The sidecar connects to this socket to fetch X.509 SVIDs.
142 pub workload_api_socket: String,
143 /// The SVID refresh interval in seconds (default 300). The client
144 /// refreshes the SVID (and trust bundle) before expiry, at half
145 /// the remaining lifetime by default; this is the upper bound on
146 /// the refresh cadence. Validated to be > 0.
147 #[serde(
148 default = "default_spiffe_svid_refresh_interval_secs",
149 skip_serializing_if = "is_default_spiffe_svid_refresh_interval_secs"
150 )]
151 pub svid_refresh_interval_secs: u64,
152}
153
154fn default_spiffe_svid_refresh_interval_secs() -> u64 {
155 300
156}
157
158fn is_default_spiffe_svid_refresh_interval_secs(v: &u64) -> bool {
159 *v == 300
160}
161
162fn is_false(b: &bool) -> bool {
163 !b
164}