Skip to main content

eggress_config/
compile.rs

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