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}