WEB: share immutable profiles + decoys across exact host lists

This commit is contained in:
Alexey
2026-10-15 07:05:36 +03:00
parent 05a367259a
commit 831cd0f1a7
51 changed files with 1661 additions and 343 deletions
+49 -16
View File
@@ -1,21 +1,57 @@
use super::*;
/// Bounds expanded routing tables before normalizing exact hostname selectors.
pub(super) fn validate_vhosts(config: &mut ProxyConfig) -> Result<()> {
let limits = &config.web.limits;
if config.web.vhosts.len() > limits.max_vhosts {
return config_error("web.vhosts exceeds web.limits.max_vhosts");
}
let mut hosts = HashSet::with_capacity(config.web.vhosts.len());
let mut host_count = 0usize;
let mut profile_count = 0usize;
for (vhost_idx, vhost) in config.web.vhosts.iter().enumerate() {
let count = match (&vhost.host, &vhost.hosts) {
(Some(_), None) => 1,
(None, Some(hosts)) if !hosts.is_empty() => hosts.len(),
_ => {
return config_error(&format!(
"web.vhosts[{vhost_idx}] requires exactly one of host or non-empty hosts"
));
}
};
host_count = host_count
.checked_add(count)
.ok_or_else(|| ProxyError::Config("WEB hostname count overflowed usize".to_string()))?;
profile_count = count
.checked_mul(vhost.profiles.len())
.and_then(|expanded| profile_count.checked_add(expanded))
.ok_or_else(|| ProxyError::Config("WEB profile count overflowed usize".to_string()))?;
if host_count > limits.max_vhosts {
return config_error("web.vhosts exceeds web.limits.max_vhosts");
}
if profile_count > limits.max_profiles {
return config_error("WEB profiles exceed web.limits.max_profiles");
}
}
let mut hosts = HashSet::with_capacity(host_count);
for (vhost_idx, vhost) in config.web.vhosts.iter_mut().enumerate() {
vhost.host = normalize_web_host(&vhost.host, &format!("web.vhosts[{vhost_idx}].host"))?;
let single = vhost.host.is_some();
for (host_idx, host) in vhost
.host
.iter_mut()
.chain(vhost.hosts.iter_mut().flatten())
.enumerate()
{
let field = if single {
format!("web.vhosts[{vhost_idx}].host")
} else {
format!("web.vhosts[{vhost_idx}].hosts[{host_idx}]")
};
*host = normalize_web_host(host, &field)?;
if !hosts.insert(host.clone()) {
return config_error(&format!("duplicate WEB vhost host `{host}` at {field}"));
}
}
validate_web_base_path(
&vhost.base_path,
&format!("web.vhosts[{vhost_idx}].base_path"),
)?;
if !hosts.insert(vhost.host.clone()) {
return config_error(&format!("duplicate WEB vhost host `{}`", vhost.host));
}
if vhost.public_addr.port() != 443 || vhost.public_addr.ip().is_unspecified() {
return config_error(&format!(
"web.vhosts[{vhost_idx}].public_addr must be a concrete socket address on port 443"
@@ -42,8 +78,8 @@ pub(super) fn validate_vhosts(config: &mut ProxyConfig) -> Result<()> {
}
if !profiles.insert((profile.user.as_str(), profile.secret_mode)) {
return config_error(&format!(
"duplicate WEB profile for user `{}` in vhost `{}`",
profile.user, vhost.host
"duplicate WEB profile for user `{}` in web.vhosts[{vhost_idx}]",
profile.user
));
}
let max_streams = profile.max_streams.unwrap_or(limits.max_streams_global);
@@ -68,14 +104,8 @@ pub(super) fn validate_vhosts(config: &mut ProxyConfig) -> Result<()> {
"web.vhosts[{vhost_idx}].profiles[{profile_idx}] limits must be non-zero and within global WEB limits"
));
}
profile_count = profile_count.checked_add(1).ok_or_else(|| {
ProxyError::Config("WEB profile count overflowed usize".to_string())
})?;
}
}
if profile_count > limits.max_profiles {
return config_error("WEB profiles exceed web.limits.max_profiles");
}
Ok(())
}
@@ -98,6 +128,7 @@ fn validate_web_base_path(value: &str, field: &str) -> Result<()> {
))
}
/// Applies the client-compatible hostname rules only during configuration loading.
pub(super) fn normalize_web_host(value: &str, field: &str) -> Result<String> {
let input = value.trim();
if input.is_empty()
@@ -137,6 +168,7 @@ pub(super) fn normalize_web_host(value: &str, field: &str) -> Result<String> {
Ok(host)
}
/// Rejects numeric suffixes that clients may interpret as address literals.
pub(super) fn web_host_last_label_is_numeric(host: &str) -> bool {
let label = host.rsplit('.').next().unwrap_or_default();
let digits = label
@@ -148,6 +180,7 @@ pub(super) fn web_host_last_label_is_numeric(host: &str) -> bool {
label.bytes().all(|byte| byte.is_ascii_digit())
}
/// Validates one group's ordinary-site origin or bounded static snapshot source.
pub(super) fn validate_decoy(vhost_idx: usize, decoy: &WebDecoyConfig) -> Result<()> {
match decoy {
WebDecoyConfig::HttpUpstream { upstream, resolve } => {