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#[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#[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#[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#[derive(Debug, Clone)]
112pub struct CompiledUnixListenerConfig {
113 pub path: std::path::PathBuf,
114 pub unlink_existing: bool,
115 pub mode: u32,
116}
117
118#[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#[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 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
924fn 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
938fn 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 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
1785fn 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}