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