Skip to main content

eggress_config/compile/
model.rs

1//! Compiled runtime model: `RuntimeConfig` and compiled DTOs.
2//!
3//! `parse -> validate -> compile` remains one-way: compilation modules may
4//! assume validation invariants only where the current design already
5//! guarantees them.
6
7use zeroize::Zeroize;
8
9use eggress_core::ProtocolId;
10use eggress_routing::scheduler::SchedulerKind;
11use eggress_routing::UpstreamGroupId;
12
13/// Compiled TLS material for a native reverse server control channel.
14#[derive(Clone)]
15pub struct CompiledReverseServerTls {
16    pub cert_pem: Vec<u8>,
17    pub key_pem: Vec<u8>,
18    pub client_ca_pem: Option<Vec<u8>>,
19    pub require_client_cert: bool,
20}
21
22impl std::fmt::Debug for CompiledReverseServerTls {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("CompiledReverseServerTls")
25            .field("has_cert", &!self.cert_pem.is_empty())
26            .field("has_key", &!self.key_pem.is_empty())
27            .field("has_client_ca", &self.client_ca_pem.is_some())
28            .field("require_client_cert", &self.require_client_cert)
29            .finish()
30    }
31}
32
33impl Drop for CompiledReverseServerTls {
34    fn drop(&mut self) {
35        self.key_pem.zeroize();
36        if let Some(ref mut ca) = self.client_ca_pem {
37            ca.zeroize();
38        }
39    }
40}
41
42/// Compiled TLS material for a native reverse client control channel.
43#[derive(Clone)]
44pub struct CompiledReverseClientTls {
45    pub ca_pem: Option<Vec<u8>>,
46    pub server_name: String,
47    pub client_cert_pem: Option<Vec<u8>>,
48    pub client_key_pem: Option<Vec<u8>>,
49}
50
51impl std::fmt::Debug for CompiledReverseClientTls {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("CompiledReverseClientTls")
54            .field("has_ca", &self.ca_pem.is_some())
55            .field("server_name", &self.server_name)
56            .field("has_client_cert", &self.client_cert_pem.is_some())
57            .field("has_client_key", &self.client_key_pem.is_some())
58            .finish()
59    }
60}
61
62impl Drop for CompiledReverseClientTls {
63    fn drop(&mut self) {
64        if let Some(ref mut key) = self.client_key_pem {
65            key.zeroize();
66        }
67    }
68}
69
70/// Compiled reverse server configuration with resolved defaults and parsed addresses.
71#[derive(Clone)]
72pub struct CompiledReverseServerConfig {
73    pub id: String,
74    pub control_bind: std::net::SocketAddr,
75    pub external_bind: std::net::SocketAddr,
76    pub auth_username: Option<String>,
77    pub auth_password: Option<String>,
78    pub max_control_connections: u32,
79    pub read_timeout_ms: u64,
80    pub allow_bind: Option<Vec<std::net::SocketAddr>>,
81    pub max_listeners_per_client: u32,
82    pub max_streams_per_listener: u32,
83    pub max_pending_external: u32,
84    pub pproxy_compat: bool,
85    pub tls: Option<CompiledReverseServerTls>,
86}
87
88impl std::fmt::Debug for CompiledReverseServerConfig {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        // `auth_password` is redacted so debug logging of the compiled
91        // runtime config can never leak credentials.
92        f.debug_struct("CompiledReverseServerConfig")
93            .field("id", &self.id)
94            .field("control_bind", &self.control_bind)
95            .field("external_bind", &self.external_bind)
96            .field("auth_username", &self.auth_username)
97            .field("auth_password", &"****")
98            .field("max_control_connections", &self.max_control_connections)
99            .field("read_timeout_ms", &self.read_timeout_ms)
100            .field("allow_bind", &self.allow_bind)
101            .field("max_listeners_per_client", &self.max_listeners_per_client)
102            .field("max_streams_per_listener", &self.max_streams_per_listener)
103            .field("max_pending_external", &self.max_pending_external)
104            .field("pproxy_compat", &self.pproxy_compat)
105            .field("tls", &self.tls)
106            .finish()
107    }
108}
109
110impl Drop for CompiledReverseServerConfig {
111    fn drop(&mut self) {
112        if let Some(password) = &mut self.auth_password {
113            password.zeroize();
114        }
115    }
116}
117
118/// Compiled reverse client configuration with resolved defaults and parsed addresses.
119#[derive(Clone)]
120pub struct CompiledReverseClientConfig {
121    pub id: String,
122    pub server_addr: std::net::SocketAddr,
123    pub server_chain: Option<eggress_uri::ProxyChainSpec>,
124    pub auth_username: Option<String>,
125    pub auth_password: Option<String>,
126    pub reconnect_initial_ms: u64,
127    pub reconnect_max_ms: u64,
128    pub default_target_host: Option<String>,
129    pub default_target_port: Option<u16>,
130    pub read_timeout_ms: u64,
131    pub drain_grace_ms: u64,
132    pub parallel_connections: u32,
133    pub pproxy_compat: bool,
134    pub tls: Option<CompiledReverseClientTls>,
135}
136
137impl std::fmt::Debug for CompiledReverseClientConfig {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        // `auth_password` is redacted; see CompiledReverseServerConfig.
140        f.debug_struct("CompiledReverseClientConfig")
141            .field("id", &self.id)
142            .field("server_addr", &self.server_addr)
143            .field("server_chain", &self.server_chain)
144            .field("auth_username", &self.auth_username)
145            .field("auth_password", &"****")
146            .field("reconnect_initial_ms", &self.reconnect_initial_ms)
147            .field("reconnect_max_ms", &self.reconnect_max_ms)
148            .field("default_target_host", &self.default_target_host)
149            .field("default_target_port", &self.default_target_port)
150            .field("read_timeout_ms", &self.read_timeout_ms)
151            .field("drain_grace_ms", &self.drain_grace_ms)
152            .field("parallel_connections", &self.parallel_connections)
153            .field("pproxy_compat", &self.pproxy_compat)
154            .field("tls", &self.tls)
155            .finish()
156    }
157}
158
159impl Drop for CompiledReverseClientConfig {
160    fn drop(&mut self) {
161        if let Some(password) = &mut self.auth_password {
162            password.zeroize();
163        }
164    }
165}
166
167#[derive(Debug, Clone)]
168pub struct RuntimeConfig {
169    pub process: ProcessConfig,
170    pub timeouts: TimeoutConfig,
171    pub listeners: Vec<ListenerConfig>,
172    pub upstreams: Vec<UpstreamConfig>,
173    pub groups: Vec<UpstreamGroupConfig>,
174    pub rules: Vec<eggress_routing::CompiledRule>,
175    pub default_action: eggress_routing::RouteActionSpec,
176    pub admin: Option<AdminConfig>,
177    pub reverse_servers: Vec<CompiledReverseServerConfig>,
178    pub reverse_clients: Vec<CompiledReverseClientConfig>,
179}
180
181#[derive(Debug, Clone)]
182pub struct ProcessConfig {
183    pub log_format: String,
184    pub log_level: String,
185    pub shutdown_grace: std::time::Duration,
186}
187
188impl Default for ProcessConfig {
189    fn default() -> Self {
190        Self {
191            log_format: "text".to_string(),
192            log_level: "info".to_string(),
193            shutdown_grace: std::time::Duration::from_secs(30),
194        }
195    }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct TimeoutConfig {
200    pub handshake: std::time::Duration,
201    pub connect: std::time::Duration,
202}
203
204impl Default for TimeoutConfig {
205    fn default() -> Self {
206        Self {
207            handshake: std::time::Duration::from_secs(10),
208            connect: std::time::Duration::from_secs(30),
209        }
210    }
211}
212
213/// Compiled transparent proxy configuration with resolved defaults.
214#[derive(Debug, Clone)]
215pub struct CompiledTransparentConfig {
216    pub enabled: bool,
217    pub protocol: String,
218}
219
220impl Default for CompiledTransparentConfig {
221    fn default() -> Self {
222        Self {
223            enabled: false,
224            protocol: "redir".to_string(),
225        }
226    }
227}
228
229/// Compiled Unix domain socket listener configuration with resolved defaults.
230#[derive(Debug, Clone)]
231pub struct CompiledUnixListenerConfig {
232    pub path: std::path::PathBuf,
233    pub unlink_existing: bool,
234    pub mode: u32,
235}
236
237/// Compiled UDP listener configuration with resolved defaults.
238#[derive(Debug, Clone)]
239pub struct CompiledListenerUdpConfig {
240    pub mode: eggress_udp::UdpMode,
241    pub enabled: bool,
242    pub bind: std::net::SocketAddr,
243    pub advertise: Option<std::net::IpAddr>,
244    pub idle_timeout: std::time::Duration,
245    pub target_idle_timeout: std::time::Duration,
246    pub max_associations: usize,
247    pub max_targets_per_association: usize,
248    pub max_datagram_size: usize,
249    pub client_pin: bool,
250    pub allow_private_egress: bool,
251    pub max_associations_global: usize,
252    pub fixed_target: Option<eggress_core::TargetAddr>,
253    pub upstream_connect_timeout: std::time::Duration,
254    pub upstream_udp_bind: std::net::SocketAddr,
255}
256
257impl Default for CompiledListenerUdpConfig {
258    fn default() -> Self {
259        Self {
260            mode: eggress_udp::UdpMode::Socks5UdpAssociate,
261            enabled: true,
262            bind: "127.0.0.1:0".parse().unwrap(),
263            advertise: None,
264            idle_timeout: std::time::Duration::from_secs(60),
265            target_idle_timeout: std::time::Duration::from_secs(30),
266            max_associations: 1024,
267            max_targets_per_association: 64,
268            max_datagram_size: 65535,
269            client_pin: true,
270            allow_private_egress: true,
271            max_associations_global: 1024,
272            fixed_target: None,
273            upstream_connect_timeout: std::time::Duration::from_secs(10),
274            upstream_udp_bind: "127.0.0.1:0".parse().unwrap(),
275        }
276    }
277}
278
279#[derive(Debug, Clone)]
280pub struct ListenerConfig {
281    pub name: String,
282    pub bind: String,
283    pub protocols: Vec<ProtocolId>,
284    pub reuse_port: Option<bool>,
285    pub connection_limit: Option<u32>,
286    pub auth: Option<crate::model::AuthConfig>,
287    pub udp: Option<CompiledListenerUdpConfig>,
288    pub tls: Option<CompiledListenerTlsConfig>,
289    pub shadowsocks: Option<crate::model::ShadowsocksListenerConfig>,
290    pub trojan: Option<crate::model::ListenerTrojanConfig>,
291    pub transparent: Option<CompiledTransparentConfig>,
292    pub unix: Option<CompiledUnixListenerConfig>,
293    pub fixed_target: Option<eggress_core::TargetAddr>,
294    pub local_bind: Option<String>,
295}
296
297/// Compiled TLS configuration for a listener.
298#[derive(Debug, Clone)]
299pub struct CompiledListenerTlsConfig {
300    pub cert_pem: Vec<u8>,
301    pub key_pem: Vec<u8>,
302    pub alpn: Vec<Vec<u8>>,
303}
304
305#[derive(Debug, Clone)]
306pub struct CompiledH2Config {
307    pub max_concurrent_streams: u32,
308    pub pool_size: u32,
309    pub idle_timeout: std::time::Duration,
310    pub keepalive_interval: std::time::Duration,
311    pub keepalive_timeout: std::time::Duration,
312    pub stream_receive_window: u32,
313    pub connection_receive_window: u32,
314    pub max_frame_size: u32,
315    pub max_header_list_size: u32,
316}
317
318impl Default for CompiledH2Config {
319    fn default() -> Self {
320        Self {
321            max_concurrent_streams: 100,
322            pool_size: 4,
323            idle_timeout: std::time::Duration::from_secs(60),
324            keepalive_interval: std::time::Duration::from_secs(30),
325            keepalive_timeout: std::time::Duration::from_secs(10),
326            stream_receive_window: 65535,
327            connection_receive_window: 65535,
328            max_frame_size: 16384,
329            max_header_list_size: 65535,
330        }
331    }
332}
333
334#[derive(Debug, Clone)]
335pub struct UpstreamConfig {
336    pub id: String,
337    pub chain: eggress_uri::ProxyChainSpec,
338    pub health: eggress_routing::health::HealthConfig,
339    pub h2: Option<CompiledH2Config>,
340}
341
342#[derive(Debug, Clone)]
343pub struct UpstreamGroupConfig {
344    pub id: UpstreamGroupId,
345    pub scheduler: SchedulerKind,
346    pub members: Vec<String>,
347    pub fallback: GroupFallback,
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351pub enum GroupFallback {
352    Reject,
353    Direct,
354    UseUnhealthy,
355}
356
357#[derive(Debug, Clone)]
358pub struct PacConfig {
359    pub path: String,
360    pub proxy_directive: String,
361    pub direct_fallback: bool,
362    pub direct_hosts: Vec<String>,
363    pub direct_suffixes: Vec<String>,
364}
365
366#[derive(Debug, Clone)]
367pub struct StaticRoute {
368    pub path: String,
369    pub content_type: String,
370    pub body: String,
371}
372
373#[derive(Debug, Clone)]
374pub struct AdminConfig {
375    pub bind: String,
376    pub enabled: bool,
377    pub metrics: bool,
378    pub auth: Option<AdminAuthConfig>,
379    pub pac: Option<PacConfig>,
380    pub static_content: Vec<StaticRoute>,
381}
382
383#[derive(Clone)]
384pub struct AdminAuthConfig {
385    pub bearer_token: Option<String>,
386    pub basic_username: Option<String>,
387    pub basic_password: Option<String>,
388}
389
390impl std::fmt::Debug for AdminAuthConfig {
391    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392        // `bearer_token` and `basic_password` are redacted so debug logging
393        // of the compiled admin config can never leak credentials.
394        f.debug_struct("AdminAuthConfig")
395            .field("bearer_token", &"****")
396            .field("basic_username", &self.basic_username)
397            .field("basic_password", &"****")
398            .finish()
399    }
400}
401
402impl Drop for AdminAuthConfig {
403    fn drop(&mut self) {
404        if let Some(token) = &mut self.bearer_token {
405            token.zeroize();
406        }
407        if let Some(password) = &mut self.basic_password {
408            password.zeroize();
409        }
410    }
411}