use crate::error::ConfigWarning;
use crate::model::ConfigFile;
pub(crate) const VALID_PROTOCOLS: &[&str] = &[
"http",
"socks4",
"socks5",
"shadowsocks",
"trojan",
"h2",
"h3",
"quic",
"websocket",
"ws",
"wss",
"raw",
"echo",
];
pub(crate) const VALID_SCHEDULERS: &[&str] = &[
"first-available",
"round-robin",
"random",
"least-connections",
];
pub(crate) const VALID_FALLBACKS: &[&str] = &["reject", "direct", "use-unhealthy"];
pub(crate) const VALID_AUTH_TYPES: &[&str] = &["password"];
pub(crate) const VALID_REJECT_REASONS: &[&str] = &[
"unsupported-protocol",
"auth-required",
"access-denied",
"blocked",
"internal-error",
];
pub(crate) const VALID_HEALTH_MODES: &[&str] = &["tcp_connect"];
pub(crate) const VALID_HEALTH_INITIAL_STATES: &[&str] =
&["unknown", "healthy", "unhealthy", "disabled"];
pub fn validate_config_composition(config: &ConfigFile) -> Vec<ConfigWarning> {
let mut warnings = Vec::new();
let matrix = match load_composition_matrix() {
Some(m) => m,
None => {
warnings.push(ConfigWarning {
path: "composition_matrix".to_string(),
message: "composition matrix not found relative to the working directory; \
protocol composition warnings are suppressed"
.to_string(),
});
return warnings;
}
};
let listener_protocols: Vec<&str> = config
.listeners
.as_ref()
.map(|listeners| {
listeners
.iter()
.flat_map(|l| l.protocols.iter().map(|p| p.as_str()))
.collect()
})
.unwrap_or_default();
if let Some(ref upstreams) = config.upstreams {
let upstream_chains: std::collections::HashMap<&str, eggress_uri::ProxyChainSpec> =
upstreams
.iter()
.filter_map(|u| {
eggress_uri::parse_proxy_chain(&u.uri)
.ok()
.map(|chain| (u.id.as_str(), chain))
})
.collect();
for upstream in upstreams {
if let Some(chain) = upstream_chains.get(upstream.id.as_str()) {
let caps = eggress_core::capability::classify_upstream_chain(chain);
if caps.is_tcp_supported() {
for &proto in &listener_protocols {
if !matrix_cell_supported(&matrix, proto, "listener", "tcp") {
warnings.push(ConfigWarning {
path: format!("upstreams[{}].uri", upstream.id),
message: format!(
"listener protocol '{proto}' has no TCP composition cell; \
upstream '{}' may not work",
upstream.id
),
});
}
}
}
if caps.is_udp_supported() {
for &proto in &listener_protocols {
if !matrix_cell_supported(&matrix, proto, "listener", "udp") {
warnings.push(ConfigWarning {
path: format!("upstreams[{}].uri", upstream.id),
message: format!(
"listener protocol '{proto}' has no UDP composition cell; \
upstream '{}' may not work with UDP relay",
upstream.id
),
});
}
}
}
}
}
}
warnings
}
#[derive(serde::Deserialize)]
pub(crate) struct CompositionCellMinimal {
protocol: String,
role: String,
traffic_kind: String,
tier: String,
}
#[derive(serde::Deserialize)]
pub(crate) struct CompositionMatrixMinimal {
cell: Vec<CompositionCellMinimal>,
}
pub(crate) const EMBEDDED_COMPOSITION_MATRIX: &str = include_str!("../../composition_matrix.toml");
pub(crate) fn load_composition_matrix() -> Option<CompositionMatrixMinimal> {
let candidates = [
"docs/parity/composition_matrix.toml",
"../docs/parity/composition_matrix.toml",
"../../docs/parity/composition_matrix.toml",
];
for path in &candidates {
if let Ok(content) = std::fs::read_to_string(path) {
if let Ok(matrix) = toml::from_str::<CompositionMatrixMinimal>(&content) {
return Some(matrix);
}
}
}
if let Ok(matrix) = toml::from_str::<CompositionMatrixMinimal>(EMBEDDED_COMPOSITION_MATRIX) {
return Some(matrix);
}
None
}
pub(crate) fn matrix_cell_supported(
matrix: &CompositionMatrixMinimal,
protocol: &str,
role: &str,
traffic_kind: &str,
) -> bool {
matrix.cell.iter().any(|c| {
c.protocol == protocol
&& c.role == role
&& c.traffic_kind == traffic_kind
&& c.tier != "unsupported"
})
}