Skip to main content

eggress_config/
compile.rs

1use std::sync::Arc;
2use zeroize::Zeroize;
3
4use eggress_core::{ProtocolId, RejectReason};
5use eggress_routing::scheduler::SchedulerKind;
6use eggress_routing::UpstreamGroupId;
7
8use crate::error::ConfigError;
9use crate::model::{
10    ConfigFile, HealthConfigToml, LeafMatcher, ListenerUdpConfig, MatchExprConfig, RuleConfig,
11};
12use crate::validate::validate_duration;
13
14/// Compiled reverse server configuration with resolved defaults and parsed addresses.
15#[derive(Clone)]
16pub struct CompiledReverseServerConfig {
17    pub id: String,
18    pub control_bind: std::net::SocketAddr,
19    pub external_bind: std::net::SocketAddr,
20    pub auth_username: Option<String>,
21    pub auth_password: Option<String>,
22    pub max_control_connections: u32,
23    pub read_timeout_ms: u64,
24    pub allow_bind: Option<Vec<std::net::SocketAddr>>,
25    pub max_listeners_per_client: u32,
26    pub max_streams_per_listener: u32,
27    pub max_pending_external: u32,
28    pub pproxy_compat: bool,
29}
30
31impl std::fmt::Debug for CompiledReverseServerConfig {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        // `auth_password` is redacted so debug logging of the compiled
34        // runtime config can never leak credentials.
35        f.debug_struct("CompiledReverseServerConfig")
36            .field("id", &self.id)
37            .field("control_bind", &self.control_bind)
38            .field("external_bind", &self.external_bind)
39            .field("auth_username", &self.auth_username)
40            .field("auth_password", &"****")
41            .field("max_control_connections", &self.max_control_connections)
42            .field("read_timeout_ms", &self.read_timeout_ms)
43            .field("allow_bind", &self.allow_bind)
44            .field("max_listeners_per_client", &self.max_listeners_per_client)
45            .field("max_streams_per_listener", &self.max_streams_per_listener)
46            .field("max_pending_external", &self.max_pending_external)
47            .field("pproxy_compat", &self.pproxy_compat)
48            .finish()
49    }
50}
51
52impl Drop for CompiledReverseServerConfig {
53    fn drop(&mut self) {
54        if let Some(password) = &mut self.auth_password {
55            password.zeroize();
56        }
57    }
58}
59
60/// Compiled reverse client configuration with resolved defaults and parsed addresses.
61#[derive(Clone)]
62pub struct CompiledReverseClientConfig {
63    pub id: String,
64    pub server_addr: std::net::SocketAddr,
65    pub server_chain: Option<eggress_uri::ProxyChainSpec>,
66    pub auth_username: Option<String>,
67    pub auth_password: Option<String>,
68    pub reconnect_initial_ms: u64,
69    pub reconnect_max_ms: u64,
70    pub default_target_host: Option<String>,
71    pub default_target_port: Option<u16>,
72    pub read_timeout_ms: u64,
73    pub drain_grace_ms: u64,
74    pub parallel_connections: u32,
75    pub pproxy_compat: bool,
76}
77
78impl std::fmt::Debug for CompiledReverseClientConfig {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        // `auth_password` is redacted; see CompiledReverseServerConfig.
81        f.debug_struct("CompiledReverseClientConfig")
82            .field("id", &self.id)
83            .field("server_addr", &self.server_addr)
84            .field("server_chain", &self.server_chain)
85            .field("auth_username", &self.auth_username)
86            .field("auth_password", &"****")
87            .field("reconnect_initial_ms", &self.reconnect_initial_ms)
88            .field("reconnect_max_ms", &self.reconnect_max_ms)
89            .field("default_target_host", &self.default_target_host)
90            .field("default_target_port", &self.default_target_port)
91            .field("read_timeout_ms", &self.read_timeout_ms)
92            .field("drain_grace_ms", &self.drain_grace_ms)
93            .field("parallel_connections", &self.parallel_connections)
94            .field("pproxy_compat", &self.pproxy_compat)
95            .finish()
96    }
97}
98
99impl Drop for CompiledReverseClientConfig {
100    fn drop(&mut self) {
101        if let Some(password) = &mut self.auth_password {
102            password.zeroize();
103        }
104    }
105}
106
107#[derive(Debug, Clone)]
108pub struct RuntimeConfig {
109    pub process: ProcessConfig,
110    pub timeouts: TimeoutConfig,
111    pub listeners: Vec<ListenerConfig>,
112    pub upstreams: Vec<UpstreamConfig>,
113    pub groups: Vec<UpstreamGroupConfig>,
114    pub rules: Vec<eggress_routing::CompiledRule>,
115    pub default_action: eggress_routing::RouteActionSpec,
116    pub admin: Option<AdminConfig>,
117    pub reverse_servers: Vec<CompiledReverseServerConfig>,
118    pub reverse_clients: Vec<CompiledReverseClientConfig>,
119}
120
121#[derive(Debug, Clone)]
122pub struct ProcessConfig {
123    pub log_format: String,
124    pub log_level: String,
125    pub shutdown_grace: std::time::Duration,
126}
127
128impl Default for ProcessConfig {
129    fn default() -> Self {
130        Self {
131            log_format: "text".to_string(),
132            log_level: "info".to_string(),
133            shutdown_grace: std::time::Duration::from_secs(30),
134        }
135    }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct TimeoutConfig {
140    pub handshake: std::time::Duration,
141    pub connect: std::time::Duration,
142}
143
144impl Default for TimeoutConfig {
145    fn default() -> Self {
146        Self {
147            handshake: std::time::Duration::from_secs(10),
148            connect: std::time::Duration::from_secs(30),
149        }
150    }
151}
152
153/// Compiled transparent proxy configuration with resolved defaults.
154#[derive(Debug, Clone)]
155pub struct CompiledTransparentConfig {
156    pub enabled: bool,
157    pub protocol: String,
158}
159
160impl Default for CompiledTransparentConfig {
161    fn default() -> Self {
162        Self {
163            enabled: false,
164            protocol: "redir".to_string(),
165        }
166    }
167}
168
169/// Compiled Unix domain socket listener configuration with resolved defaults.
170#[derive(Debug, Clone)]
171pub struct CompiledUnixListenerConfig {
172    pub path: std::path::PathBuf,
173    pub unlink_existing: bool,
174    pub mode: u32,
175}
176
177/// Compiled UDP listener configuration with resolved defaults.
178#[derive(Debug, Clone)]
179pub struct CompiledListenerUdpConfig {
180    pub mode: eggress_udp::UdpMode,
181    pub enabled: bool,
182    pub bind: std::net::SocketAddr,
183    pub advertise: Option<std::net::IpAddr>,
184    pub idle_timeout: std::time::Duration,
185    pub target_idle_timeout: std::time::Duration,
186    pub max_associations: usize,
187    pub max_targets_per_association: usize,
188    pub max_datagram_size: usize,
189    pub client_pin: bool,
190    pub allow_private_egress: bool,
191    pub max_associations_global: usize,
192    pub fixed_target: Option<eggress_core::TargetAddr>,
193    pub upstream_connect_timeout: std::time::Duration,
194    pub upstream_udp_bind: std::net::SocketAddr,
195}
196
197impl Default for CompiledListenerUdpConfig {
198    fn default() -> Self {
199        Self {
200            mode: eggress_udp::UdpMode::Socks5UdpAssociate,
201            enabled: true,
202            bind: "127.0.0.1:0".parse().unwrap(),
203            advertise: None,
204            idle_timeout: std::time::Duration::from_secs(60),
205            target_idle_timeout: std::time::Duration::from_secs(30),
206            max_associations: 1024,
207            max_targets_per_association: 64,
208            max_datagram_size: 65535,
209            client_pin: true,
210            allow_private_egress: true,
211            max_associations_global: 1024,
212            fixed_target: None,
213            upstream_connect_timeout: std::time::Duration::from_secs(10),
214            upstream_udp_bind: "127.0.0.1:0".parse().unwrap(),
215        }
216    }
217}
218
219#[derive(Debug, Clone)]
220pub struct ListenerConfig {
221    pub name: String,
222    pub bind: String,
223    pub protocols: Vec<ProtocolId>,
224    pub reuse_port: Option<bool>,
225    pub connection_limit: Option<u32>,
226    pub auth: Option<crate::model::AuthConfig>,
227    pub udp: Option<CompiledListenerUdpConfig>,
228    pub tls: Option<CompiledListenerTlsConfig>,
229    pub shadowsocks: Option<crate::model::ShadowsocksListenerConfig>,
230    pub trojan: Option<crate::model::ListenerTrojanConfig>,
231    pub transparent: Option<CompiledTransparentConfig>,
232    pub unix: Option<CompiledUnixListenerConfig>,
233    pub fixed_target: Option<eggress_core::TargetAddr>,
234    pub local_bind: Option<String>,
235}
236
237/// Compiled TLS configuration for a listener.
238#[derive(Debug, Clone)]
239pub struct CompiledListenerTlsConfig {
240    pub cert_pem: Vec<u8>,
241    pub key_pem: Vec<u8>,
242    pub alpn: Vec<Vec<u8>>,
243}
244
245#[derive(Debug, Clone)]
246pub struct CompiledH2Config {
247    pub max_concurrent_streams: u32,
248    pub pool_size: u32,
249    pub idle_timeout: std::time::Duration,
250    pub keepalive_interval: std::time::Duration,
251    pub keepalive_timeout: std::time::Duration,
252    pub stream_receive_window: u32,
253    pub connection_receive_window: u32,
254    pub max_frame_size: u32,
255    pub max_header_list_size: u32,
256}
257
258impl Default for CompiledH2Config {
259    fn default() -> Self {
260        Self {
261            max_concurrent_streams: 100,
262            pool_size: 4,
263            idle_timeout: std::time::Duration::from_secs(60),
264            keepalive_interval: std::time::Duration::from_secs(30),
265            keepalive_timeout: std::time::Duration::from_secs(10),
266            stream_receive_window: 65535,
267            connection_receive_window: 65535,
268            max_frame_size: 16384,
269            max_header_list_size: 65535,
270        }
271    }
272}
273
274#[derive(Debug, Clone)]
275pub struct UpstreamConfig {
276    pub id: String,
277    pub chain: eggress_uri::ProxyChainSpec,
278    pub health: eggress_routing::health::HealthConfig,
279    pub h2: Option<CompiledH2Config>,
280}
281
282#[derive(Debug, Clone)]
283pub struct UpstreamGroupConfig {
284    pub id: UpstreamGroupId,
285    pub scheduler: SchedulerKind,
286    pub members: Vec<String>,
287    pub fallback: GroupFallback,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum GroupFallback {
292    Reject,
293    Direct,
294    UseUnhealthy,
295}
296
297#[derive(Debug, Clone)]
298pub struct PacConfig {
299    pub path: String,
300    pub proxy_directive: String,
301    pub direct_fallback: bool,
302    pub direct_hosts: Vec<String>,
303    pub direct_suffixes: Vec<String>,
304}
305
306#[derive(Debug, Clone)]
307pub struct StaticRoute {
308    pub path: String,
309    pub content_type: String,
310    pub body: String,
311}
312
313#[derive(Debug, Clone)]
314pub struct AdminConfig {
315    pub bind: String,
316    pub enabled: bool,
317    pub metrics: bool,
318    pub auth: Option<AdminAuthConfig>,
319    pub pac: Option<PacConfig>,
320    pub static_content: Vec<StaticRoute>,
321}
322
323#[derive(Clone)]
324pub struct AdminAuthConfig {
325    pub bearer_token: Option<String>,
326    pub basic_username: Option<String>,
327    pub basic_password: Option<String>,
328}
329
330impl std::fmt::Debug for AdminAuthConfig {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        // `bearer_token` and `basic_password` are redacted so debug logging
333        // of the compiled admin config can never leak credentials.
334        f.debug_struct("AdminAuthConfig")
335            .field("bearer_token", &"****")
336            .field("basic_username", &self.basic_username)
337            .field("basic_password", &"****")
338            .finish()
339    }
340}
341
342impl Drop for AdminAuthConfig {
343    fn drop(&mut self) {
344        if let Some(token) = &mut self.bearer_token {
345            token.zeroize();
346        }
347        if let Some(password) = &mut self.basic_password {
348            password.zeroize();
349        }
350    }
351}
352
353fn compile_reject_reason(s: &str) -> Result<RejectReason, ConfigError> {
354    match s {
355        "unsupported-protocol" => Ok(RejectReason::UnsupportedProtocol),
356        "auth-required" => Ok(RejectReason::AuthRequired),
357        "access-denied" => Ok(RejectReason::AccessDenied),
358        "blocked" => Ok(RejectReason::Blocked),
359        "internal-error" => Ok(RejectReason::InternalError),
360        _ => Err(ConfigError::validation(
361            "reject",
362            &format!("unknown reject reason: {}", s),
363        )),
364    }
365}
366
367fn compile_protocol(s: &str) -> Result<ProtocolId, ConfigError> {
368    match s {
369        "http" => Ok(ProtocolId::Http),
370        "httponly" => Ok(ProtocolId::Http),
371        "socks4" => Ok(ProtocolId::Socks4),
372        "socks5" => Ok(ProtocolId::Socks5),
373        "shadowsocks" => Ok(ProtocolId::Shadowsocks),
374        "ssr" => Ok(ProtocolId::ShadowsocksR),
375        "trojan" => Ok(ProtocolId::Trojan),
376        "h2" => Ok(ProtocolId::Http2),
377        "h3" => {
378            #[cfg(feature = "quic")]
379            {
380                Ok(ProtocolId::Http3)
381            }
382            #[cfg(not(feature = "quic"))]
383            {
384                Err(ConfigError::validation(
385                    "protocols",
386                    "HTTP/3 requires the optional 'quic' feature",
387                ))
388            }
389        }
390        "quic" => {
391            #[cfg(feature = "quic")]
392            {
393                Ok(ProtocolId::Quic)
394            }
395            #[cfg(not(feature = "quic"))]
396            {
397                Err(ConfigError::validation(
398                    "protocols",
399                    "QUIC requires the optional 'quic' feature",
400                ))
401            }
402        }
403        "websocket" | "ws" | "wss" => Ok(ProtocolId::WebSocket),
404        "raw" | "tunnel" => Ok(ProtocolId::Raw),
405        "echo" => Ok(ProtocolId::Echo),
406        _ => Err(ConfigError::validation(
407            "protocols",
408            &format!("unknown protocol: {}", s),
409        )),
410    }
411}
412
413fn compile_transport(s: &str) -> Result<eggress_routing::TransportKind, ConfigError> {
414    match s {
415        "tcp" => Ok(eggress_routing::TransportKind::Tcp),
416        "udp" => Ok(eggress_routing::TransportKind::Udp),
417        "reverse_tcp" => Ok(eggress_routing::TransportKind::ReverseTcp),
418        _ => Err(ConfigError::validation(
419            "transport",
420            &format!("unknown transport: {}", s),
421        )),
422    }
423}
424
425fn compile_matcher(rule: &RuleConfig) -> Result<eggress_routing::MatchExpr, ConfigError> {
426    if let Some(ref match_expr) = rule.match_expr {
427        return compile_match_config(match_expr);
428    }
429
430    if let Some(ref exact) = rule.host_exact {
431        if rule.host_suffix.is_none()
432            && rule.host_regex.is_none()
433            && rule.destination_port.is_none()
434            && rule.destination_port_regex.is_none()
435            && !rule.any.unwrap_or(false)
436        {
437            return Ok(eggress_routing::MatchExpr::HostExact(Arc::from(
438                eggress_routing::normalize_host_for_exact(exact),
439            )));
440        }
441    }
442    if let Some(ref suffix) = rule.host_suffix {
443        if rule.host_exact.is_none()
444            && rule.host_regex.is_none()
445            && rule.destination_port.is_none()
446            && rule.destination_port_regex.is_none()
447            && !rule.any.unwrap_or(false)
448        {
449            return Ok(eggress_routing::MatchExpr::HostSuffix(Arc::from(
450                suffix.as_str(),
451            )));
452        }
453    }
454    if let Some(ref regex_str) = rule.host_regex {
455        if rule.host_exact.is_none()
456            && rule.host_suffix.is_none()
457            && rule.destination_port.is_none()
458            && rule.destination_port_regex.is_none()
459            && !rule.any.unwrap_or(false)
460        {
461            let re = regex::Regex::new(regex_str).map_err(|e| {
462                ConfigError::validation(
463                    "host_regex",
464                    &format!("invalid regex '{}': {}", regex_str, e),
465                )
466            })?;
467            return Ok(eggress_routing::MatchExpr::HostRegex(re));
468        }
469    }
470    if let Some(ref regex_str) = rule.destination_port_regex {
471        let re = regex::Regex::new(regex_str).map_err(|e| {
472            ConfigError::validation(
473                "destination_port_regex",
474                &format!("invalid regex '{}': {}", regex_str, e),
475            )
476        })?;
477        if rule.host_exact.is_none()
478            && rule.host_suffix.is_none()
479            && rule.host_regex.is_none()
480            && rule.destination_port.is_none()
481            && !rule.any.unwrap_or(false)
482        {
483            return Ok(eggress_routing::MatchExpr::DestinationPortRegex(re));
484        }
485    }
486    if let Some(port) = rule.destination_port {
487        if rule.host_exact.is_none()
488            && rule.host_suffix.is_none()
489            && rule.host_regex.is_none()
490            && rule.destination_port_regex.is_none()
491            && !rule.any.unwrap_or(false)
492        {
493            return Ok(eggress_routing::MatchExpr::DestinationPort(
494                eggress_routing::PortMatcher::Exact(port),
495            ));
496        }
497    }
498    if rule.any.unwrap_or(false)
499        || (rule.host_exact.is_none()
500            && rule.host_suffix.is_none()
501            && rule.host_regex.is_none()
502            && rule.destination_port.is_none())
503    {
504        return Ok(eggress_routing::MatchExpr::Any);
505    }
506    Err(ConfigError::validation(&rule.id, "ambiguous matcher"))
507}
508
509const MAX_EXPRESSION_DEPTH: usize = 10;
510const MAX_NODE_COUNT: usize = 100;
511
512fn compile_match_config(
513    config: &MatchExprConfig,
514) -> Result<eggress_routing::MatchExpr, ConfigError> {
515    let mut node_count = 0;
516    compile_match_config_limited(config, 0, &mut node_count)
517}
518
519fn compile_match_config_limited(
520    config: &MatchExprConfig,
521    depth: usize,
522    node_count: &mut usize,
523) -> Result<eggress_routing::MatchExpr, ConfigError> {
524    *node_count += 1;
525    if *node_count > MAX_NODE_COUNT {
526        return Err(ConfigError::validation(
527            "match",
528            &format!("expression exceeds maximum node count ({})", MAX_NODE_COUNT),
529        ));
530    }
531    if depth >= MAX_EXPRESSION_DEPTH {
532        return Err(ConfigError::validation(
533            "match",
534            &format!(
535                "expression exceeds maximum depth ({})",
536                MAX_EXPRESSION_DEPTH
537            ),
538        ));
539    }
540
541    match config {
542        MatchExprConfig::Composite(composite) => {
543            if let Some(ref all) = composite.all {
544                if all.is_empty() {
545                    return Err(ConfigError::validation("match.all", "must not be empty"));
546                }
547                let exprs: Vec<eggress_routing::MatchExpr> = all
548                    .iter()
549                    .map(|c| compile_match_config_limited(c, depth + 1, node_count))
550                    .collect::<Result<Vec<_>, _>>()?;
551                return Ok(eggress_routing::MatchExpr::All(exprs));
552            }
553            if let Some(ref any_of) = composite.any_of {
554                if any_of.is_empty() {
555                    return Err(ConfigError::validation("match.any_of", "must not be empty"));
556                }
557                let exprs: Vec<eggress_routing::MatchExpr> = any_of
558                    .iter()
559                    .map(|c| compile_match_config_limited(c, depth + 1, node_count))
560                    .collect::<Result<Vec<_>, _>>()?;
561                return Ok(eggress_routing::MatchExpr::AnyOf(exprs));
562            }
563            if let Some(ref not) = composite.not {
564                let inner = compile_match_config_limited(not, depth + 1, node_count)?;
565                return Ok(eggress_routing::MatchExpr::Not(Box::new(inner)));
566            }
567            Err(ConfigError::validation(
568                "match",
569                "composite must have exactly one of: all, any_of, not",
570            ))
571        }
572        MatchExprConfig::Leaf(leaf) => compile_leaf_matcher(leaf),
573    }
574}
575
576fn compile_leaf_matcher(leaf: &LeafMatcher) -> Result<eggress_routing::MatchExpr, ConfigError> {
577    let mut matchers = Vec::new();
578
579    if let Some(ref exact) = leaf.host_exact {
580        matchers.push(eggress_routing::MatchExpr::HostExact(Arc::from(
581            eggress_routing::normalize_host_for_exact(exact),
582        )));
583    }
584    if let Some(ref suffix) = leaf.host_suffix {
585        matchers.push(eggress_routing::MatchExpr::HostSuffix(Arc::from(
586            suffix.as_str(),
587        )));
588    }
589    if let Some(ref regex_str) = leaf.host_regex {
590        let re = regex::Regex::new(regex_str).map_err(|e| {
591            ConfigError::validation(
592                "host_regex",
593                &format!("invalid regex '{}': {}", regex_str, e),
594            )
595        })?;
596        matchers.push(eggress_routing::MatchExpr::HostRegex(re));
597    }
598    if let Some(ref regex_str) = leaf.destination_port_regex {
599        let re = regex::Regex::new(regex_str).map_err(|e| {
600            ConfigError::validation(
601                "destination_port_regex",
602                &format!("invalid regex '{}': {}", regex_str, e),
603            )
604        })?;
605        matchers.push(eggress_routing::MatchExpr::DestinationPortRegex(re));
606    }
607    if let Some(port) = leaf.destination_port {
608        matchers.push(eggress_routing::MatchExpr::DestinationPort(
609            eggress_routing::PortMatcher::Exact(port),
610        ));
611    }
612    if let Some(ref range) = leaf.destination_port_range {
613        if range.len() != 2 {
614            return Err(ConfigError::validation(
615                "destination_port_range",
616                "must have exactly 2 elements [start, end]",
617            ));
618        }
619        let matcher = eggress_routing::PortMatcher::new_range(range[0], range[1])
620            .map_err(|e| ConfigError::validation("destination_port_range", &e))?;
621        matchers.push(eggress_routing::MatchExpr::DestinationPort(matcher));
622    }
623    if let Some(ref ports) = leaf.destination_port_set {
624        if ports.is_empty() {
625            return Err(ConfigError::validation(
626                "destination_port_set",
627                "must not be empty",
628            ));
629        }
630        let matcher = eggress_routing::PortMatcher::new_set(ports.clone());
631        matchers.push(eggress_routing::MatchExpr::DestinationPort(matcher));
632    }
633    if let Some(ref cidr) = leaf.destination_cidr {
634        let net: ipnet::IpNet = cidr.parse().map_err(|e: ipnet::AddrParseError| {
635            ConfigError::validation(
636                "destination_cidr",
637                &format!("invalid CIDR '{}': {}", cidr, e),
638            )
639        })?;
640        matchers.push(eggress_routing::MatchExpr::DestinationCidr(net));
641    }
642    if let Some(ref cidr) = leaf.source_cidr {
643        let net: ipnet::IpNet = cidr.parse().map_err(|e: ipnet::AddrParseError| {
644            ConfigError::validation("source_cidr", &format!("invalid CIDR '{}': {}", cidr, e))
645        })?;
646        matchers.push(eggress_routing::MatchExpr::SourceCidr(net));
647    }
648    if let Some(source_port) = leaf.source_port {
649        matchers.push(eggress_routing::MatchExpr::SourcePort(
650            eggress_routing::PortMatcher::Exact(source_port),
651        ));
652    }
653    if let Some(ref name) = leaf.listener {
654        matchers.push(eggress_routing::MatchExpr::Listener(Arc::from(
655            name.as_str(),
656        )));
657    }
658    if let Some(ref proto) = leaf.protocol {
659        let protocol_id = compile_protocol(proto)?;
660        matchers.push(eggress_routing::MatchExpr::Protocol(protocol_id));
661    }
662    if let Some(ref ident) = leaf.identity {
663        matchers.push(eggress_routing::MatchExpr::Identity(Arc::from(
664            ident.as_str(),
665        )));
666    }
667    if let Some(ref transport_str) = leaf.transport {
668        let transport_kind = compile_transport(transport_str)?;
669        matchers.push(eggress_routing::MatchExpr::Transport(transport_kind));
670    }
671    if let Some(ref name) = leaf.reverse_listener {
672        matchers.push(eggress_routing::MatchExpr::ReverseListener(Arc::from(
673            name.as_str(),
674        )));
675    }
676
677    match matchers.len() {
678        0 => Ok(eggress_routing::MatchExpr::Any),
679        1 => Ok(matchers.into_iter().next().expect("len checked to be 1")),
680        _ => Ok(eggress_routing::MatchExpr::All(matchers)),
681    }
682}
683
684fn compile_action(
685    rule: &RuleConfig,
686    group_ids: &std::collections::HashSet<&str>,
687) -> Result<eggress_routing::RouteActionSpec, ConfigError> {
688    if let Some(direct) = rule.direct {
689        if direct {
690            return Ok(eggress_routing::RouteActionSpec::Direct);
691        }
692        return Err(ConfigError::validation(
693            &rule.id,
694            "direct action must be true",
695        ));
696    }
697    if let Some(ref group) = rule.upstream_group {
698        if !group_ids.contains(group.as_str()) {
699            return Err(ConfigError::validation(
700                &rule.id,
701                &format!("unknown upstream group: {}", group),
702            ));
703        }
704        return Ok(eggress_routing::RouteActionSpec::UpstreamGroup(
705            UpstreamGroupId(Arc::from(group.as_str())),
706        ));
707    }
708    if let Some(ref reject) = rule.reject {
709        let reason = compile_reject_reason(reject)?;
710        return Ok(eggress_routing::RouteActionSpec::Reject(reason));
711    }
712    Err(ConfigError::validation(&rule.id, "missing action"))
713}
714
715pub fn compile_config(config: &ConfigFile) -> Result<RuntimeConfig, ConfigError> {
716    let process = compile_process(config);
717    let timeouts = compile_timeouts(config)?;
718    let listeners = compile_listeners(config)?;
719    let upstreams = compile_upstreams(config)?;
720    let groups = compile_groups(config)?;
721    let rules = compile_rules(config)?;
722    let default_action = compile_default_action(config);
723    let admin = compile_admin(config)?;
724    let reverse_servers = compile_reverse_servers(config)?;
725    let reverse_clients = compile_reverse_clients(config)?;
726
727    Ok(RuntimeConfig {
728        process,
729        timeouts,
730        listeners,
731        upstreams,
732        groups,
733        rules,
734        default_action,
735        admin,
736        reverse_servers,
737        reverse_clients,
738    })
739}
740
741fn compile_process(config: &ConfigFile) -> ProcessConfig {
742    let defaults = ProcessConfig::default();
743    let process = config.process.as_ref();
744
745    ProcessConfig {
746        log_format: process
747            .and_then(|p| p.log_format.clone())
748            .unwrap_or(defaults.log_format),
749        log_level: process
750            .and_then(|p| p.log_level.clone())
751            .unwrap_or(defaults.log_level),
752        shutdown_grace: process
753            .and_then(|p| p.shutdown_grace.as_ref())
754            .and_then(|s| parse_duration_opt(s))
755            .unwrap_or(defaults.shutdown_grace),
756    }
757}
758
759fn compile_timeouts(config: &ConfigFile) -> Result<TimeoutConfig, ConfigError> {
760    let defaults = TimeoutConfig::default();
761    let timeouts = config.timeouts.as_ref();
762
763    Ok(TimeoutConfig {
764        handshake: timeouts
765            .and_then(|t| t.handshake.as_ref())
766            .map(|s| validate_duration(s))
767            .transpose()?
768            .unwrap_or(defaults.handshake),
769        connect: timeouts
770            .and_then(|t| t.connect.as_ref())
771            .map(|s| validate_duration(s))
772            .transpose()?
773            .unwrap_or(defaults.connect),
774    })
775}
776
777fn compile_listeners(config: &ConfigFile) -> Result<Vec<ListenerConfig>, ConfigError> {
778    let listeners = match &config.listeners {
779        Some(l) => l,
780        None => return Ok(vec![]),
781    };
782
783    listeners
784        .iter()
785        .enumerate()
786        .map(|(i, l)| {
787            let path = format!("listeners[{}]", i);
788
789            let protocols: Vec<ProtocolId> = l
790                .protocols
791                .iter()
792                .map(|p| compile_protocol(p))
793                .collect::<Result<Vec<_>, _>>()?;
794
795            if protocols.is_empty() {
796                return Err(ConfigError::validation(
797                    &path,
798                    "protocols must not be empty",
799                ));
800            }
801
802            if protocols.contains(&ProtocolId::ShadowsocksR) {
803                let ssr = l.ssr.as_ref().ok_or_else(|| {
804                    ConfigError::validation(
805                        &format!("{}.ssr", path),
806                        "ssr protocol requires an [listeners.ssr] section",
807                    )
808                })?;
809                for (plugin_index, plugin) in ssr.plugins.iter().enumerate() {
810                    if !matches!(
811                        plugin.as_str(),
812                        "plain"
813                            | "origin"
814                            | "http_simple"
815                            | "tls1.2_ticket_auth"
816                            | "verify_simple"
817                            | "verify_deflate"
818                    ) {
819                        return Err(ConfigError::validation(
820                            &format!("{}.ssr.plugins[{}]", path, plugin_index),
821                            "unknown pproxy plugin; expected plain, origin, http_simple, tls1.2_ticket_auth, verify_simple, or verify_deflate",
822                        ));
823                    }
824                }
825            }
826
827            let udp = match (l.udp_enabled, l.udp.as_ref()) {
828                (None, None) => None,
829                (None, Some(udp_cfg)) => {
830                    Some(compile_listener_udp_config(udp_cfg, &protocols, &path)?)
831                }
832                (Some(true), None) => Some(compile_listener_udp_defaults(&protocols, &path)?),
833                (Some(true), Some(udp_cfg)) => {
834                    Some(compile_listener_udp_config(udp_cfg, &protocols, &path)?)
835                }
836                (Some(false), None) => None,
837                (Some(false), Some(udp_cfg)) => {
838                    if udp_cfg.enabled.unwrap_or(true) {
839                        return Err(ConfigError::validation(
840                            &path,
841                            "udp_enabled = false conflicts with [listeners.udp] enabled = true",
842                        ));
843                    }
844                    Some(compile_listener_udp_config(udp_cfg, &protocols, &path)?)
845                }
846            };
847
848            if let Some(ref udp_cfg) = udp {
849                if udp_cfg.mode == eggress_udp::UdpMode::ShadowsocksUdp {
850                    let ss = l.shadowsocks.as_ref().ok_or_else(|| {
851                        ConfigError::validation(
852                            &path,
853                            "shadowsocks_udp mode requires [listeners.shadowsocks] section with method and password",
854                        )
855                    })?;
856                    if ss.method.is_empty() {
857                        return Err(ConfigError::validation(
858                            &format!("{}.shadowsocks.method", path),
859                            "shadowsocks method must not be empty",
860                        ));
861                    }
862                    if ss.password.is_empty() {
863                        return Err(ConfigError::validation(
864                            &format!("{}.shadowsocks.password", path),
865                            "shadowsocks password must not be empty",
866                        ));
867                    }
868                }
869            }
870
871            let tls = match l.tls.as_ref() {
872                Some(tls_cfg) => {
873                    let cert_pem = std::fs::read(&tls_cfg.cert).map_err(|e| {
874                        ConfigError::validation(
875                            &format!("{}.tls.cert", path),
876                            &format!("failed to read cert file: {}", e),
877                        )
878                    })?;
879                    let key_pem = std::fs::read(&tls_cfg.key).map_err(|e| {
880                        ConfigError::validation(
881                            &format!("{}.tls.key", path),
882                            &format!("failed to read key file: {}", e),
883                        )
884                    })?;
885                    // Validate PEM at compile time
886                    let mut builder = eggress_transport_tls::TlsServerConfigBuilder::new()
887                        .with_certificate_pem(&cert_pem)
888                        .and_then(|b| b.with_key_pem(&key_pem));
889                    if let Some(ref alpn) = tls_cfg.alpn {
890                        let alpn_bytes: Vec<Vec<u8>> =
891                            alpn.iter().map(|s| s.as_bytes().to_vec()).collect();
892                        builder = builder.map(|b| b.with_alpn(alpn_bytes));
893                    }
894                    builder.map_err(|e| {
895                        ConfigError::validation(
896                            &format!("{}.tls", path),
897                            &format!("invalid TLS config: {}", e),
898                        )
899                    })?;
900                    let alpn = tls_cfg
901                        .alpn
902                        .as_ref()
903                        .map(|protocols| protocols.iter().map(|p| p.as_bytes().to_vec()).collect())
904                        .unwrap_or_default();
905                    Some(CompiledListenerTlsConfig {
906                        cert_pem,
907                        key_pem,
908                        alpn,
909                    })
910                }
911                None => None,
912            };
913
914            if protocols.contains(&ProtocolId::Quic) || protocols.contains(&ProtocolId::Http3) {
915                if tls.is_none() {
916                    return Err(ConfigError::validation(
917                        &format!("{}.tls", path),
918                        "QUIC/HTTP3 listeners require certificate and key material",
919                    ));
920                }
921                if l.unix.is_some() || l.transparent.as_ref().is_some_and(|t| t.enabled.unwrap_or(false)) {
922                    return Err(ConfigError::validation(
923                        &path,
924                        "QUIC/HTTP3 listeners cannot use unix or transparent listener modes",
925                    ));
926                }
927                let application_protocols = protocols
928                    .iter()
929                    .filter(|protocol| !matches!(protocol, ProtocolId::Quic | ProtocolId::Http3))
930                    .count();
931                if protocols.contains(&ProtocolId::Http3) && protocols.len() != 1 {
932                    return Err(ConfigError::validation(
933                        &format!("{}.protocols", path),
934                        "HTTP/3 listeners must use exactly the h3 protocol",
935                    ));
936                }
937                if protocols.contains(&ProtocolId::Quic) && application_protocols == 0 {
938                    return Err(ConfigError::validation(
939                        &format!("{}.protocols", path),
940                        "raw QUIC listeners require an application protocol such as http or socks5",
941                    ));
942                }
943                if udp.is_some() {
944                    return Err(ConfigError::validation(
945                        &format!("{}.udp", path),
946                        "QUIC and HTTP/3 listeners do not provide UDP association mode",
947                    ));
948                }
949                if l.fixed_target.is_some() && protocols.contains(&ProtocolId::Http3) {
950                    return Err(ConfigError::validation(
951                        &format!("{}.fixed_target", path),
952                        "HTTP/3 CONNECT uses the request authority instead of a fixed target",
953                    ));
954                }
955            }
956
957            let transparent = compile_transparent_config(l.transparent.as_ref())?;
958
959            let unix = compile_unix_listener_config(l.unix.as_ref())?;
960            let fixed_target = l.fixed_target.as_deref().map(|value| value.parse().map_err(|e: String| ConfigError::validation(&format!("{}.fixed_target", path), &e))).transpose()?;
961
962            let auth = l.auth.as_ref().map(|a| -> Result<_, ConfigError> {
963                let resolved_password = resolve_password(
964                    a.password.as_deref(),
965                    a.password_env.as_deref(),
966                    &path,
967                )?;
968                Ok(crate::model::AuthConfig {
969                    auth_type: a.auth_type.clone(),
970                    username: a.username.clone(),
971                    password: resolved_password,
972                    password_env: None,
973                })
974            })
975            .transpose()?;
976
977            Ok(ListenerConfig {
978                name: l.name.clone(),
979                bind: l.bind.clone(),
980                protocols,
981                reuse_port: l.reuse_port,
982                connection_limit: l.connection_limit,
983                auth,
984                udp,
985                tls,
986                shadowsocks: l.shadowsocks.clone().or_else(|| {
987                    l.ssr.as_ref().map(|ssr| crate::model::ShadowsocksListenerConfig {
988                        method: "ssr".to_string(),
989                        password: String::new(),
990                        auth_prefix: ssr.auth_prefix.clone(),
991                        plugins: ssr.plugins.clone(),
992                    })
993                }),
994                trojan: l.trojan.clone(),
995                transparent,
996                unix,
997                fixed_target,
998                local_bind: l.local_bind.clone(),
999            })
1000        })
1001        .collect()
1002}
1003
1004/// Compile default UDP config when `udp_enabled = true` but no `[listeners.udp]` section.
1005fn compile_listener_udp_defaults(
1006    protocols: &[ProtocolId],
1007    path: &str,
1008) -> Result<CompiledListenerUdpConfig, ConfigError> {
1009    if !protocols.contains(&ProtocolId::Socks5) {
1010        return Err(ConfigError::validation(
1011            path,
1012            "udp_enabled = true requires socks5 protocol",
1013        ));
1014    }
1015    Ok(CompiledListenerUdpConfig::default())
1016}
1017
1018/// Compile a `[listeners.udp]` section into `CompiledListenerUdpConfig`.
1019fn compile_listener_udp_config(
1020    udp: &ListenerUdpConfig,
1021    protocols: &[ProtocolId],
1022    path: &str,
1023) -> Result<CompiledListenerUdpConfig, ConfigError> {
1024    let defaults = CompiledListenerUdpConfig::default();
1025    let udp_path = format!("{}.udp", path);
1026
1027    let mode = match udp.mode.as_deref() {
1028        Some("standalone_pproxy_udp") | Some("standalone") => {
1029            eggress_udp::UdpMode::StandalonePproxyUdp
1030        }
1031        Some("shadowsocks_udp") | Some("shadowsocks") => eggress_udp::UdpMode::ShadowsocksUdp,
1032        Some("echo") => eggress_udp::UdpMode::Echo,
1033        Some("fixed_target") | Some("fixed-target") => eggress_udp::UdpMode::FixedTarget,
1034        Some("socks5_udp_associate") | Some("socks5") | None => {
1035            eggress_udp::UdpMode::Socks5UdpAssociate
1036        }
1037        Some(other) => {
1038            return Err(ConfigError::validation(
1039                &format!("{}.mode", udp_path),
1040                &format!(
1041                    "unknown UDP mode '{}'; expected 'socks5_udp_associate', 'standalone_pproxy_udp', 'shadowsocks_udp', or 'echo'",
1042                    other
1043                ),
1044            ));
1045        }
1046    };
1047
1048    if mode == eggress_udp::UdpMode::Socks5UdpAssociate && !protocols.contains(&ProtocolId::Socks5)
1049    {
1050        return Err(ConfigError::validation(
1051            path,
1052            "UDP config requires socks5 protocol",
1053        ));
1054    }
1055
1056    if mode == eggress_udp::UdpMode::Echo && !protocols.contains(&ProtocolId::Echo) {
1057        return Err(ConfigError::validation(
1058            path,
1059            "echo UDP mode requires echo protocol",
1060        ));
1061    }
1062
1063    let fixed_target = udp
1064        .fixed_target
1065        .as_deref()
1066        .map(|value| {
1067            value.parse().map_err(|e: String| {
1068                ConfigError::validation(&format!("{}.udp.fixed_target", path), &e)
1069            })
1070        })
1071        .transpose()?;
1072    if mode == eggress_udp::UdpMode::FixedTarget && fixed_target.is_none() {
1073        return Err(ConfigError::validation(
1074            &format!("{}.udp.fixed_target", path),
1075            "fixed_target UDP mode requires a target",
1076        ));
1077    }
1078
1079    if mode == eggress_udp::UdpMode::ShadowsocksUdp {
1080        let has_ss_section = protocols.contains(&ProtocolId::Shadowsocks);
1081        if !has_ss_section {
1082            return Err(ConfigError::validation(
1083                path,
1084                "shadowsocks_udp mode requires shadowsocks protocol",
1085            ));
1086        }
1087        if !udp.client_pin.unwrap_or(true) {
1088            return Err(ConfigError::validation(
1089                &format!("{}.udp.client_pin", path),
1090                "shadowsocks_udp mode requires client_pin = true for security",
1091            ));
1092        }
1093    }
1094
1095    let enabled = udp.enabled.unwrap_or(defaults.enabled);
1096    if !enabled {
1097        return Ok(CompiledListenerUdpConfig {
1098            enabled: false,
1099            ..defaults
1100        });
1101    }
1102
1103    let bind_str = udp.bind.as_deref().unwrap_or("127.0.0.1:0");
1104    let bind: std::net::SocketAddr = bind_str.parse().map_err(|_| {
1105        ConfigError::validation(
1106            &format!("{}.bind", udp_path),
1107            &format!("invalid socket address: {}", bind_str),
1108        )
1109    })?;
1110
1111    let advertise = match &udp.advertise {
1112        Some(addr_str) => {
1113            let ip: std::net::IpAddr = addr_str.parse().map_err(|_| {
1114                ConfigError::validation(
1115                    &format!("{}.advertise", udp_path),
1116                    &format!("invalid IP address: {}", addr_str),
1117                )
1118            })?;
1119            Some(ip)
1120        }
1121        None => None,
1122    };
1123
1124    let idle_timeout = udp
1125        .idle_timeout
1126        .as_deref()
1127        .map(validate_duration)
1128        .transpose()
1129        .map_err(|e| {
1130            ConfigError::validation(&format!("{}.idle_timeout", udp_path), &e.to_string())
1131        })?
1132        .unwrap_or(defaults.idle_timeout);
1133
1134    let target_idle_timeout = udp
1135        .target_idle_timeout
1136        .as_deref()
1137        .map(validate_duration)
1138        .transpose()
1139        .map_err(|e| {
1140            ConfigError::validation(&format!("{}.target_idle_timeout", udp_path), &e.to_string())
1141        })?
1142        .unwrap_or(defaults.target_idle_timeout);
1143
1144    let max_associations = udp.max_associations.unwrap_or(defaults.max_associations);
1145    if max_associations == 0 {
1146        return Err(ConfigError::validation(
1147            &format!("{}.max_associations", udp_path),
1148            "must be greater than 0",
1149        ));
1150    }
1151
1152    let max_targets_per_association = udp
1153        .max_targets_per_association
1154        .unwrap_or(defaults.max_targets_per_association);
1155    if max_targets_per_association == 0 {
1156        return Err(ConfigError::validation(
1157            &format!("{}.max_targets_per_association", udp_path),
1158            "must be greater than 0",
1159        ));
1160    }
1161
1162    let max_datagram_size = udp.max_datagram_size.unwrap_or(defaults.max_datagram_size);
1163    if !(257..=65535).contains(&max_datagram_size) {
1164        return Err(ConfigError::validation(
1165            &format!("{}.max_datagram_size", udp_path),
1166            &format!("must be between 257 and 65535, got {}", max_datagram_size),
1167        ));
1168    }
1169
1170    let client_pin = udp.client_pin.unwrap_or(defaults.client_pin);
1171
1172    let allow_private_egress = udp
1173        .allow_private_egress
1174        .unwrap_or(defaults.allow_private_egress);
1175
1176    let max_associations_global = udp
1177        .max_associations_global
1178        .unwrap_or(defaults.max_associations_global);
1179    if max_associations_global == 0 {
1180        return Err(ConfigError::validation(
1181            &format!("{}.max_associations_global", udp_path),
1182            "must be greater than 0",
1183        ));
1184    }
1185
1186    let upstream_connect_timeout = udp
1187        .upstream_connect_timeout
1188        .as_deref()
1189        .map(validate_duration)
1190        .transpose()
1191        .map_err(|e| {
1192            ConfigError::validation(
1193                &format!("{}.upstream_connect_timeout", udp_path),
1194                &e.to_string(),
1195            )
1196        })?
1197        .unwrap_or(defaults.upstream_connect_timeout);
1198
1199    let upstream_udp_bind_str = udp.upstream_udp_bind.as_deref().unwrap_or("127.0.0.1:0");
1200    let upstream_udp_bind: std::net::SocketAddr = upstream_udp_bind_str.parse().map_err(|_| {
1201        ConfigError::validation(
1202            &format!("{}.upstream_udp_bind", udp_path),
1203            &format!("invalid socket address: {}", upstream_udp_bind_str),
1204        )
1205    })?;
1206
1207    Ok(CompiledListenerUdpConfig {
1208        mode,
1209        enabled,
1210        bind,
1211        advertise,
1212        idle_timeout,
1213        target_idle_timeout,
1214        max_associations,
1215        max_targets_per_association,
1216        max_datagram_size,
1217        client_pin,
1218        allow_private_egress,
1219        max_associations_global,
1220        fixed_target,
1221        upstream_connect_timeout,
1222        upstream_udp_bind,
1223    })
1224}
1225
1226fn compile_health_config(
1227    health: Option<&HealthConfigToml>,
1228) -> Result<eggress_routing::health::HealthConfig, ConfigError> {
1229    let defaults = eggress_routing::health::HealthConfig::default();
1230    let Some(h) = health else {
1231        return Ok(defaults);
1232    };
1233
1234    let interval = h
1235        .interval
1236        .as_deref()
1237        .map(validate_duration)
1238        .transpose()?
1239        .unwrap_or(defaults.interval);
1240
1241    let timeout = h
1242        .timeout
1243        .as_deref()
1244        .map(validate_duration)
1245        .transpose()?
1246        .unwrap_or(defaults.timeout);
1247
1248    let failures_to_unhealthy = h
1249        .failures_to_unhealthy
1250        .unwrap_or(defaults.failures_to_unhealthy);
1251    if failures_to_unhealthy == 0 {
1252        return Err(ConfigError::validation(
1253            "health.failures_to_unhealthy",
1254            "must be greater than 0",
1255        ));
1256    }
1257
1258    let successes_to_healthy = h
1259        .successes_to_healthy
1260        .unwrap_or(defaults.successes_to_healthy);
1261    if successes_to_healthy == 0 {
1262        return Err(ConfigError::validation(
1263            "health.successes_to_healthy",
1264            "must be greater than 0",
1265        ));
1266    }
1267
1268    let initial_state = match h.initial_state.as_deref() {
1269        Some("unknown") | None => defaults.initial_state,
1270        Some("healthy") => eggress_routing::health::HealthState::Healthy,
1271        Some("unhealthy") => eggress_routing::health::HealthState::Unhealthy,
1272        Some("disabled") => eggress_routing::health::HealthState::Disabled,
1273        Some(other) => {
1274            return Err(ConfigError::validation(
1275                "health.initial_state",
1276                &format!(
1277                    "unknown state '{}', must be one of: unknown, healthy, unhealthy, disabled",
1278                    other
1279                ),
1280            ));
1281        }
1282    };
1283
1284    Ok(eggress_routing::health::HealthConfig {
1285        interval,
1286        timeout,
1287        failures_to_unhealthy,
1288        successes_to_healthy,
1289        initial_state,
1290    })
1291}
1292
1293fn compile_h2_config(
1294    model: &crate::model::H2UpstreamConfig,
1295    path_prefix: &str,
1296) -> Result<CompiledH2Config, ConfigError> {
1297    let mut compiled = CompiledH2Config::default();
1298
1299    if let Some(max) = model.max_concurrent_streams {
1300        if max == 0 {
1301            return Err(ConfigError::validation(
1302                &format!("{}.max_concurrent_streams", path_prefix),
1303                "must be greater than 0",
1304            ));
1305        }
1306        compiled.max_concurrent_streams = max;
1307    }
1308
1309    if let Some(pool) = model.pool_size {
1310        if pool == 0 {
1311            return Err(ConfigError::validation(
1312                &format!("{}.pool_size", path_prefix),
1313                "must be greater than 0",
1314            ));
1315        }
1316        compiled.pool_size = pool;
1317    }
1318
1319    if let Some(ref idle) = model.idle_timeout {
1320        compiled.idle_timeout = validate_duration(idle).map_err(|e| {
1321            ConfigError::validation(&format!("{}.idle_timeout", path_prefix), &e.to_string())
1322        })?;
1323    }
1324
1325    if let Some(ref interval) = model.keepalive_interval {
1326        compiled.keepalive_interval = validate_duration(interval).map_err(|e| {
1327            ConfigError::validation(
1328                &format!("{}.keepalive_interval", path_prefix),
1329                &e.to_string(),
1330            )
1331        })?;
1332    }
1333
1334    if let Some(ref timeout) = model.keepalive_timeout {
1335        compiled.keepalive_timeout = validate_duration(timeout).map_err(|e| {
1336            ConfigError::validation(
1337                &format!("{}.keepalive_timeout", path_prefix),
1338                &e.to_string(),
1339            )
1340        })?;
1341    }
1342
1343    if let Some(window) = model.stream_receive_window {
1344        if window == 0 {
1345            return Err(ConfigError::validation(
1346                &format!("{}.stream_receive_window", path_prefix),
1347                "must be greater than 0",
1348            ));
1349        }
1350        compiled.stream_receive_window = window;
1351    }
1352
1353    if let Some(window) = model.connection_receive_window {
1354        if window == 0 {
1355            return Err(ConfigError::validation(
1356                &format!("{}.connection_receive_window", path_prefix),
1357                "must be greater than 0",
1358            ));
1359        }
1360        compiled.connection_receive_window = window;
1361    }
1362
1363    if let Some(size) = model.max_frame_size {
1364        if size == 0 {
1365            return Err(ConfigError::validation(
1366                &format!("{}.max_frame_size", path_prefix),
1367                "must be greater than 0",
1368            ));
1369        }
1370        compiled.max_frame_size = size;
1371    }
1372
1373    if let Some(size) = model.max_header_list_size {
1374        if size == 0 {
1375            return Err(ConfigError::validation(
1376                &format!("{}.max_header_list_size", path_prefix),
1377                "must be greater than 0",
1378            ));
1379        }
1380        compiled.max_header_list_size = size;
1381    }
1382
1383    Ok(compiled)
1384}
1385
1386fn compile_upstreams(config: &ConfigFile) -> Result<Vec<UpstreamConfig>, ConfigError> {
1387    let upstreams = match &config.upstreams {
1388        Some(u) => u,
1389        None => return Ok(vec![]),
1390    };
1391
1392    upstreams
1393        .iter()
1394        .map(|u| {
1395            eggress_routing::upstream::validate_upstream_id(&u.id)
1396                .map_err(|e| ConfigError::validation(&format!("upstream {}", u.id), &e))?;
1397
1398            let chain = eggress_uri::parse_proxy_chain(&u.uri).map_err(|_e| {
1399                ConfigError::validation(&format!("upstream {}", u.id), "invalid upstream URI")
1400            })?;
1401
1402            for (idx, hop) in chain.hops.iter().enumerate() {
1403                if hop.insecure
1404                    && (hop.protocols.contains(&eggress_uri::ProtocolSpec::Quic)
1405                        || hop.protocols.contains(&eggress_uri::ProtocolSpec::Http3))
1406                {
1407                    return Err(ConfigError::validation(
1408                        &format!("upstream {} hop {}", u.id, idx),
1409                        "insecure QUIC/H3 requires the insecure-quic feature; rebuild with --features insecure-quic",
1410                    ));
1411                }
1412            }
1413
1414            let health = compile_health_config(u.health.as_ref()).map_err(|e| match e {
1415                ConfigError::Validation { path, message } => {
1416                    ConfigError::validation(&format!("upstream {}.{}", u.id, path), &message)
1417                }
1418                other => other,
1419            })?;
1420
1421            let h2 = match u.h2.as_ref() {
1422                Some(h2_model) => Some(
1423                    compile_h2_config(h2_model, &format!("upstream {}", u.id)).map_err(
1424                        |e| match e {
1425                            ConfigError::Validation { path, message } => {
1426                                ConfigError::validation(&path, &message)
1427                            }
1428                            other => other,
1429                        },
1430                    )?,
1431                ),
1432                None => None,
1433            };
1434
1435            Ok(UpstreamConfig {
1436                id: u.id.clone(),
1437                chain,
1438                health,
1439                h2,
1440            })
1441        })
1442        .collect()
1443}
1444
1445fn compile_groups(config: &ConfigFile) -> Result<Vec<UpstreamGroupConfig>, ConfigError> {
1446    let groups = match &config.upstream_groups {
1447        Some(g) => g,
1448        None => return Ok(vec![]),
1449    };
1450
1451    groups
1452        .iter()
1453        .map(|g| {
1454            let scheduler = match g.scheduler.as_deref() {
1455                Some("round-robin") | None => SchedulerKind::RoundRobin,
1456                Some("first-available") => SchedulerKind::FirstAvailable,
1457                Some("random") => SchedulerKind::Random,
1458                Some("least-connections") => SchedulerKind::LeastConnections,
1459                Some(other) => {
1460                    return Err(ConfigError::validation(
1461                        &format!("group {}", g.id),
1462                        &format!("unknown scheduler: {}", other),
1463                    ))
1464                }
1465            };
1466
1467            let fallback = match g.fallback.as_deref() {
1468                Some("reject") | None => GroupFallback::Reject,
1469                Some("direct") => GroupFallback::Direct,
1470                Some("use-unhealthy") => GroupFallback::UseUnhealthy,
1471                Some(other) => {
1472                    return Err(ConfigError::validation(
1473                        &format!("group {}", g.id),
1474                        &format!("unknown fallback: {}", other),
1475                    ))
1476                }
1477            };
1478
1479            Ok(UpstreamGroupConfig {
1480                id: UpstreamGroupId(Arc::from(g.id.as_str())),
1481                scheduler,
1482                members: g.members.clone(),
1483                fallback,
1484            })
1485        })
1486        .collect()
1487}
1488
1489fn compile_rules(config: &ConfigFile) -> Result<Vec<eggress_routing::CompiledRule>, ConfigError> {
1490    let mut compiled_rules = Vec::new();
1491
1492    let group_ids: std::collections::HashSet<&str> = config
1493        .upstream_groups
1494        .as_ref()
1495        .map(|gs| gs.iter().map(|g| g.id.as_str()).collect())
1496        .unwrap_or_default();
1497
1498    if let Some(ref rules) = config.rules {
1499        for r in rules {
1500            let matcher = compile_matcher(r)?;
1501            let action = compile_action(r, &group_ids)?;
1502
1503            compiled_rules.push(eggress_routing::CompiledRule {
1504                id: eggress_routing::RuleId(Arc::from(r.id.as_str())),
1505                matcher,
1506                action,
1507            });
1508        }
1509    }
1510
1511    if let Some(ref rules_file_path) = config.rules_file {
1512        if group_ids.len() > 1 {
1513            return Err(ConfigError::validation(
1514                "rules_file",
1515                "rules_file routes all rules to a single group; multiple groups are not supported with rules_file — use explicit [[rules]] instead",
1516            ));
1517        }
1518        let content = crate::file::load_rules_file(rules_file_path).map_err(|e| {
1519            ConfigError::validation(
1520                "rules_file",
1521                &format!("failed to read '{}': {}", rules_file_path, e),
1522            )
1523        })?;
1524        let compat_rules = eggress_routing::CompatRegexRule::parse_file(&content).map_err(|e| {
1525            ConfigError::validation(
1526                "rules_file",
1527                &format!("failed to parse '{}': {}", rules_file_path, e),
1528            )
1529        })?;
1530        for (idx, compat) in compat_rules.into_iter().enumerate() {
1531            compiled_rules.push(eggress_routing::CompiledRule {
1532                id: eggress_routing::RuleId(Arc::from(format!("rules-file-{}", idx + 1).as_str())),
1533                matcher: eggress_routing::MatchExpr::HostRegex(compat.pattern),
1534                action: group_ids
1535                    .iter()
1536                    .next()
1537                    .map(|g| {
1538                        eggress_routing::RouteActionSpec::UpstreamGroup(
1539                            eggress_routing::UpstreamGroupId(Arc::from(*g)),
1540                        )
1541                    })
1542                    .unwrap_or(eggress_routing::RouteActionSpec::Direct),
1543            });
1544        }
1545    }
1546
1547    Ok(compiled_rules)
1548}
1549
1550fn compile_default_action(config: &ConfigFile) -> eggress_routing::RouteActionSpec {
1551    let default_str = config.routing.as_ref().and_then(|r| r.default.as_deref());
1552
1553    match default_str {
1554        Some("direct") => eggress_routing::RouteActionSpec::Direct,
1555        Some("reject") => eggress_routing::RouteActionSpec::Reject(RejectReason::Blocked),
1556        Some(group_id) => {
1557            eggress_routing::RouteActionSpec::UpstreamGroup(UpstreamGroupId(Arc::from(group_id)))
1558        }
1559        None => eggress_routing::RouteActionSpec::Direct,
1560    }
1561}
1562
1563fn compile_admin(config: &ConfigFile) -> Result<Option<AdminConfig>, ConfigError> {
1564    let Some(admin) = config.admin.as_ref() else {
1565        return Ok(None);
1566    };
1567
1568    let auth = admin.auth.as_ref().map(compile_admin_auth).transpose()?;
1569
1570    let pac = admin.pac.as_ref().map(|pac_toml| {
1571        let path = pac_toml.path.clone().unwrap_or_else(|| "/pac".to_string());
1572        PacConfig {
1573            path,
1574            proxy_directive: pac_toml.proxy.clone(),
1575            direct_fallback: pac_toml.direct_fallback.unwrap_or(true),
1576            direct_hosts: pac_toml.direct_hosts.clone().unwrap_or_default(),
1577            direct_suffixes: pac_toml.direct_suffixes.clone().unwrap_or_default(),
1578        }
1579    });
1580
1581    let static_content = admin
1582        .static_content
1583        .as_ref()
1584        .map(|entries| {
1585            entries
1586                .iter()
1587                .map(|entry| StaticRoute {
1588                    path: entry.path.clone(),
1589                    content_type: entry
1590                        .content_type
1591                        .clone()
1592                        .unwrap_or_else(|| "text/plain".to_string()),
1593                    body: entry.body.clone().unwrap_or_default(),
1594                })
1595                .collect()
1596        })
1597        .unwrap_or_default();
1598
1599    Ok(Some(AdminConfig {
1600        bind: admin
1601            .bind
1602            .clone()
1603            .unwrap_or_else(|| "127.0.0.1:9090".to_string()),
1604        enabled: admin.enabled.unwrap_or(true),
1605        metrics: admin.metrics.unwrap_or(true),
1606        auth,
1607        pac,
1608        static_content,
1609    }))
1610}
1611
1612fn compile_admin_auth(
1613    auth: &crate::model::AdminAuthConfig,
1614) -> Result<AdminAuthConfig, ConfigError> {
1615    let bearer_token = resolve_password(
1616        auth.bearer_token.as_deref(),
1617        auth.bearer_token_env.as_deref(),
1618        "admin.auth.bearer_token",
1619    )?;
1620
1621    if bearer_token.as_deref().is_some_and(str::is_empty) {
1622        return Err(ConfigError::validation(
1623            "admin.auth.bearer_token",
1624            "bearer token must not be empty",
1625        ));
1626    }
1627
1628    let basic = auth.basic_auth.as_ref();
1629    if bearer_token.is_some() && basic.is_some() {
1630        return Err(ConfigError::validation(
1631            "admin.auth",
1632            "configure either bearer_token or basic_auth, not both",
1633        ));
1634    }
1635
1636    let (basic_username, basic_password) = if let Some(basic) = basic {
1637        let password = resolve_password(
1638            basic.password.as_deref(),
1639            basic.password_env.as_deref(),
1640            "admin.auth.basic_auth",
1641        )?;
1642        let Some(password) = password else {
1643            return Err(ConfigError::validation(
1644                "admin.auth.basic_auth",
1645                "basic_auth requires password or password_env",
1646            ));
1647        };
1648        if basic.user.is_empty() || password.is_empty() {
1649            return Err(ConfigError::validation(
1650                "admin.auth.basic_auth",
1651                "basic_auth user and password must not be empty",
1652            ));
1653        }
1654        (Some(basic.user.clone()), Some(password))
1655    } else {
1656        (None, None)
1657    };
1658
1659    if bearer_token.is_none() && basic_username.is_none() {
1660        return Err(ConfigError::validation(
1661            "admin.auth",
1662            "authentication requires bearer_token or basic_auth",
1663        ));
1664    }
1665
1666    Ok(AdminAuthConfig {
1667        bearer_token,
1668        basic_username,
1669        basic_password,
1670    })
1671}
1672
1673fn compile_transparent_config(
1674    config: Option<&crate::model::TransparentConfig>,
1675) -> Result<Option<CompiledTransparentConfig>, ConfigError> {
1676    let Some(cfg) = config else {
1677        return Ok(None);
1678    };
1679
1680    let enabled = cfg.enabled.unwrap_or(false);
1681    let protocol = cfg.protocol.as_deref().unwrap_or("redir").to_string();
1682
1683    match protocol.as_str() {
1684        "redir" | "pf" => {}
1685        other => {
1686            return Err(ConfigError::validation(
1687                "transparent.protocol",
1688                &format!(
1689                    "unknown transparent protocol '{}'; expected 'redir' or 'pf'",
1690                    other
1691                ),
1692            ));
1693        }
1694    }
1695
1696    Ok(Some(CompiledTransparentConfig { enabled, protocol }))
1697}
1698
1699fn compile_unix_listener_config(
1700    config: Option<&crate::model::UnixListenerConfig>,
1701) -> Result<Option<CompiledUnixListenerConfig>, ConfigError> {
1702    let Some(cfg) = config else {
1703        return Ok(None);
1704    };
1705
1706    let path = std::path::PathBuf::from(&cfg.path);
1707    if path.parent().is_none() || path.parent() == Some(std::path::Path::new("")) {
1708        return Err(ConfigError::validation(
1709            "unix.path",
1710            &format!(
1711                "socket path must be absolute or have a valid parent directory: {}",
1712                cfg.path
1713            ),
1714        ));
1715    }
1716
1717    let unlink_existing = cfg.unlink_existing.unwrap_or(true);
1718    let mode = cfg.mode.unwrap_or(0o660);
1719
1720    Ok(Some(CompiledUnixListenerConfig {
1721        path,
1722        unlink_existing,
1723        mode,
1724    }))
1725}
1726
1727fn compile_reverse_servers(
1728    config: &ConfigFile,
1729) -> Result<Vec<CompiledReverseServerConfig>, ConfigError> {
1730    let servers = match &config.reverse_servers {
1731        Some(s) => s,
1732        None => return Ok(vec![]),
1733    };
1734
1735    servers
1736        .iter()
1737        .enumerate()
1738        .map(|(i, s)| {
1739            let path = format!("reverse_servers[{}]", i);
1740
1741            let control_bind: std::net::SocketAddr = s.control_bind.parse().map_err(|_| {
1742                ConfigError::validation(
1743                    &format!("{}.control_bind", path),
1744                    &format!("invalid socket address: {}", s.control_bind),
1745                )
1746            })?;
1747
1748            let external_bind: std::net::SocketAddr = s.external_bind.parse().map_err(|_| {
1749                ConfigError::validation(
1750                    &format!("{}.external_bind", path),
1751                    &format!("invalid socket address: {}", s.external_bind),
1752                )
1753            })?;
1754
1755            let auth_password = resolve_password(
1756                s.auth_password.as_deref(),
1757                s.auth_password_env.as_deref(),
1758                &path,
1759            )?;
1760
1761            if s.auth_username.is_some() != auth_password.is_some() {
1762                return Err(ConfigError::validation(
1763                    &path,
1764                    "reverse server auth requires both auth_username and auth_password",
1765                ));
1766            }
1767
1768            let max_streams = s.max_streams.unwrap_or(1024);
1769
1770            let heartbeat_interval_ms = s
1771                .heartbeat_interval
1772                .as_deref()
1773                .map(|h| validate_duration(h).map(|d| d.as_millis() as u64))
1774                .transpose()
1775                .map_err(|e| {
1776                    ConfigError::validation(&format!("{}.heartbeat_interval", path), &e.to_string())
1777                })?
1778                .unwrap_or(300_000);
1779
1780            Ok(CompiledReverseServerConfig {
1781                id: s.id.clone(),
1782                control_bind,
1783                external_bind,
1784                auth_username: s.auth_username.clone(),
1785                auth_password,
1786                max_control_connections: 256,
1787                read_timeout_ms: heartbeat_interval_ms,
1788                allow_bind: None,
1789                max_listeners_per_client: 1,
1790                max_streams_per_listener: max_streams,
1791                max_pending_external: 1024,
1792                pproxy_compat: s.pproxy_compat,
1793            })
1794        })
1795        .collect()
1796}
1797
1798fn compile_reverse_clients(
1799    config: &ConfigFile,
1800) -> Result<Vec<CompiledReverseClientConfig>, ConfigError> {
1801    let clients = match &config.reverse_clients {
1802        Some(c) => c,
1803        None => return Ok(vec![]),
1804    };
1805
1806    clients
1807        .iter()
1808        .enumerate()
1809        .map(|(i, c)| {
1810            let path = format!("reverse_clients[{}]", i);
1811
1812            let server_addr: std::net::SocketAddr = c.server_addr.parse().map_err(|_| {
1813                ConfigError::validation(
1814                    &format!("{}.server_addr", path),
1815                    &format!("invalid socket address: {}", c.server_addr),
1816                )
1817            })?;
1818            let server_chain = c
1819                .server_uri
1820                .as_deref()
1821                .map(eggress_uri::parse_proxy_chain)
1822                .transpose()
1823                .map_err(|error| {
1824                    ConfigError::validation(
1825                        &format!("{}.server_uri", path),
1826                        &format!("invalid backward chain: {error}"),
1827                    )
1828                })?;
1829
1830            let auth_password = resolve_password(
1831                c.auth_password.as_deref(),
1832                c.auth_password_env.as_deref(),
1833                &path,
1834            )?;
1835
1836            let reconnect_initial_ms = c
1837                .reconnect_initial
1838                .as_deref()
1839                .map(|d| validate_duration(d).map(|dur| dur.as_millis() as u64))
1840                .transpose()
1841                .map_err(|e| {
1842                    ConfigError::validation(&format!("{}.reconnect_initial", path), &e.to_string())
1843                })?
1844                .unwrap_or(1_000);
1845
1846            let reconnect_max_ms = c
1847                .reconnect_max
1848                .as_deref()
1849                .map(|d| validate_duration(d).map(|dur| dur.as_millis() as u64))
1850                .transpose()
1851                .map_err(|e| {
1852                    ConfigError::validation(&format!("{}.reconnect_max", path), &e.to_string())
1853                })?
1854                .unwrap_or(30_000);
1855
1856            let heartbeat_interval_ms = c
1857                .heartbeat_interval
1858                .as_deref()
1859                .map(|d| validate_duration(d).map(|dur| dur.as_millis() as u64))
1860                .transpose()
1861                .map_err(|e| {
1862                    ConfigError::validation(&format!("{}.heartbeat_interval", path), &e.to_string())
1863                })?
1864                .unwrap_or(60_000);
1865
1866            let parallel_connections = c.parallel_connections.unwrap_or(1);
1867
1868            if c.default_target_host.is_none() || c.default_target_port.is_none() {
1869                return Err(ConfigError::validation(
1870                    &path,
1871                    "reverse client requires default_target_host and default_target_port",
1872                ));
1873            }
1874
1875            Ok(CompiledReverseClientConfig {
1876                id: c.id.clone(),
1877                server_addr,
1878                server_chain,
1879                auth_username: c.auth_username.clone(),
1880                auth_password,
1881                reconnect_initial_ms,
1882                reconnect_max_ms,
1883                default_target_host: c.default_target_host.clone(),
1884                default_target_port: c.default_target_port,
1885                // The reverse control protocol uses the client's heartbeat
1886                // interval as its read timeout; retain this compatibility
1887                // mapping until the schema gains a separate timeout field.
1888                read_timeout_ms: heartbeat_interval_ms,
1889                drain_grace_ms: 5_000,
1890                parallel_connections,
1891                pproxy_compat: c.pproxy_compat,
1892            })
1893        })
1894        .collect()
1895}
1896
1897/// Resolve a password from either an explicit value or an environment variable.
1898fn resolve_password(
1899    password: Option<&str>,
1900    password_env: Option<&str>,
1901    path: &str,
1902) -> Result<Option<String>, ConfigError> {
1903    if let Some(env_var) = password_env {
1904        std::env::var(env_var).map(Some).map_err(|_| {
1905            ConfigError::validation(
1906                path,
1907                &format!(
1908                    "environment variable '{}' not set (referenced by auth_password_env)",
1909                    env_var
1910                ),
1911            )
1912        })
1913    } else {
1914        Ok(password.map(|s| s.to_string()))
1915    }
1916}
1917
1918fn parse_duration_opt(s: &str) -> Option<std::time::Duration> {
1919    crate::validate::validate_duration(s).ok()
1920}
1921
1922pub fn load_and_compile(path: &str) -> Result<RuntimeConfig, crate::error::ConfigError> {
1923    crate::load_and_validate(path)
1924}