v0.35.0
This commit is contained in:
1 parent
2c03fcf248
commit
d3b492874b
95 files changed
+1628
-743
No files matched your search
+74
-27
@@ -37,15 +37,34 @@ pub(crate) fn matches(name: &str, ip: IpAddr) -> bool {
|
||||
|
||||
pub(crate) fn hostname(value: &str) -> Result<String> {
|
||||
let candidate = value.strip_suffix('.').unwrap_or(value);
|
||||
ensure!(candidate.len() <= 253 && candidate.contains('.'), "ACL DNS name requires a fully qualified hostname");
|
||||
ensure!(candidate.is_ascii() && !candidate.contains('/') && !candidate.contains(':'), "invalid ACL DNS name");
|
||||
ensure!(
|
||||
candidate.len() <= 253 && candidate.contains('.'),
|
||||
"ACL DNS name requires a fully qualified hostname"
|
||||
);
|
||||
ensure!(
|
||||
candidate.is_ascii() && !candidate.contains('/') && !candidate.contains(':'),
|
||||
"invalid ACL DNS name"
|
||||
);
|
||||
let labels: Vec<_> = candidate.split('.').collect();
|
||||
ensure!(labels.last().is_some_and(|part| part.chars().any(|ch| ch.is_ascii_alphabetic())), "invalid ACL DNS name suffix");
|
||||
ensure!(
|
||||
labels
|
||||
.last()
|
||||
.is_some_and(|part| part.chars().any(|ch| ch.is_ascii_alphabetic())),
|
||||
"invalid ACL DNS name suffix"
|
||||
);
|
||||
for label in labels {
|
||||
ensure!(!label.is_empty() && label.len() <= 63, "invalid ACL DNS label length");
|
||||
ensure!(label.as_bytes()[0].is_ascii_alphanumeric()
|
||||
&& label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
|
||||
&& label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'), "invalid ACL DNS label");
|
||||
ensure!(
|
||||
!label.is_empty() && label.len() <= 63,
|
||||
"invalid ACL DNS label length"
|
||||
);
|
||||
ensure!(
|
||||
label.as_bytes()[0].is_ascii_alphanumeric()
|
||||
&& label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
|
||||
&& label
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'),
|
||||
"invalid ACL DNS label"
|
||||
);
|
||||
}
|
||||
Ok(candidate.to_ascii_lowercase())
|
||||
}
|
||||
@@ -55,27 +74,42 @@ pub(crate) fn configured_hostnames(config: &Config) -> Result<Vec<String>> {
|
||||
let mut inspect = |acl: Option<&crate::acl::Acl>| -> Result<()> {
|
||||
if let Some(acl) = acl {
|
||||
for rule in &acl.rules {
|
||||
if rule.network.parse::<ipnet::IpNet>().is_ok() || rule.network.parse::<IpAddr>().is_ok() {
|
||||
if rule.network.parse::<ipnet::IpNet>().is_ok()
|
||||
|| rule.network.parse::<IpAddr>().is_ok()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let name = hostname(&rule.network)?;
|
||||
ensure!(rule.action == crate::acl::Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use static IP/CIDR for deny");
|
||||
ensure!(
|
||||
rule.action == crate::acl::Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use static IP/CIDR for deny"
|
||||
);
|
||||
names.insert(name);
|
||||
ensure!(names.len() <= MAX_NAMES, "too many distinct dynamic DNS ACL names (max 128)");
|
||||
ensure!(
|
||||
names.len() <= MAX_NAMES,
|
||||
"too many distinct dynamic DNS ACL names (max 128)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
inspect(config.global_security.acl.as_ref())?;
|
||||
for policy in &config.host_policies { inspect(Some(&policy.acl))?; }
|
||||
for profile in &config.acl_profiles { inspect(profile.acl.as_ref())?; }
|
||||
for policy in &config.host_policies {
|
||||
inspect(Some(&policy.acl))?;
|
||||
}
|
||||
for profile in &config.acl_profiles {
|
||||
inspect(profile.acl.as_ref())?;
|
||||
}
|
||||
for vhost in &config.vhosts {
|
||||
inspect(vhost.acl.as_ref())?;
|
||||
for route in &vhost.routes { inspect(route.acl.as_ref())?; }
|
||||
for route in &vhost.routes {
|
||||
inspect(route.acl.as_ref())?;
|
||||
}
|
||||
}
|
||||
for agent in &config.docker_agents.agents {
|
||||
for assignment in &agent.assignments { inspect(assignment.acl.as_ref())?; }
|
||||
for assignment in &agent.assignments {
|
||||
inspect(assignment.acl.as_ref())?;
|
||||
}
|
||||
}
|
||||
Ok(names.into_iter().collect())
|
||||
}
|
||||
@@ -85,7 +119,8 @@ async fn resolve(name: String) -> (String, Option<Addresses>) {
|
||||
let answer = tokio::time::timeout(
|
||||
Duration::from_secs(LOOKUP_SECONDS),
|
||||
tokio::net::lookup_host((lookup_name.as_str(), 0u16)),
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
let mut ips = match answer {
|
||||
Ok(Ok(sockets)) => sockets.map(|socket| socket.ip()).collect::<Vec<_>>(),
|
||||
_ => {
|
||||
@@ -95,21 +130,29 @@ async fn resolve(name: String) -> (String, Option<Addresses>) {
|
||||
};
|
||||
ips.sort_unstable();
|
||||
ips.dedup();
|
||||
if ips.is_empty() || ips.len() > MAX_ADDRESSES || ips.iter().any(|ip| {
|
||||
matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some())
|
||||
}) {
|
||||
if ips.is_empty()
|
||||
|| ips.len() > MAX_ADDRESSES
|
||||
|| ips
|
||||
.iter()
|
||||
.any(|ip| matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some()))
|
||||
{
|
||||
tracing::warn!(hostname = %name, "dynamic ACL DNS returned an invalid or excessive address set");
|
||||
return (name, None);
|
||||
}
|
||||
(name, Some(Addresses { ips, valid_until: Instant::now() + Duration::from_secs(MAX_AGE_SECONDS) }))
|
||||
(
|
||||
name,
|
||||
Some(Addresses {
|
||||
ips,
|
||||
valid_until: Instant::now() + Duration::from_secs(MAX_AGE_SECONDS),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn config_changed() { CHANGED.notify_one(); }
|
||||
pub(crate) fn config_changed() {
|
||||
CHANGED.notify_one();
|
||||
}
|
||||
|
||||
pub(crate) async fn poll_loop(
|
||||
app: Weak<crate::runtime::App>,
|
||||
shutdown: CancellationToken,
|
||||
) {
|
||||
pub(crate) async fn poll_loop(app: Weak<crate::runtime::App>, shutdown: CancellationToken) {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(REFRESH_SECONDS));
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
loop {
|
||||
@@ -118,9 +161,13 @@ pub(crate) async fn poll_loop(
|
||||
_ = interval.tick() => {},
|
||||
_ = CHANGED.notified() => {},
|
||||
}
|
||||
let Some(state) = app.upgrade() else { break; };
|
||||
let Some(state) = app.upgrade() else {
|
||||
break;
|
||||
};
|
||||
let snapshot = state.config.load();
|
||||
let Ok(names) = configured_hostnames(&snapshot.envelope.config) else { continue; };
|
||||
let Ok(names) = configured_hostnames(&snapshot.envelope.config) else {
|
||||
continue;
|
||||
};
|
||||
drop(snapshot);
|
||||
let results: HashMap<_, _> = tokio::select! {
|
||||
_ = shutdown.cancelled() => break,
|
||||
|
||||
+21
-3
@@ -207,13 +207,29 @@ mod tests {
|
||||
routing_path("/static/js//events.js").unwrap(),
|
||||
"/static/js/events.js"
|
||||
);
|
||||
for (path, expected) in [
|
||||
("/x%2Fy", "/x/y"),
|
||||
("/x%2fy", "/x/y"),
|
||||
("/x%2F%2Fy", "/x/y"),
|
||||
("/x//y", "/x/y"),
|
||||
("/%2Ffoo", "/foo"),
|
||||
] {
|
||||
assert_eq!(routing_path(path).unwrap(), expected, "{path}");
|
||||
}
|
||||
for path in [
|
||||
"/%2e%2e/secret",
|
||||
"/a/%2E%2E/b",
|
||||
"/a%2fb",
|
||||
"/x%2F..%2Fy",
|
||||
"/x%2F%2E%2E%2Fy",
|
||||
"/a%5cb",
|
||||
"/%5Cadmin",
|
||||
"/%25admin",
|
||||
"/%3Fadmin",
|
||||
"/%23admin",
|
||||
"/%3Badmin",
|
||||
"/bad%",
|
||||
"/bad%2",
|
||||
"/%00",
|
||||
] {
|
||||
assert!(routing_path(path).is_err(), "{path}");
|
||||
}
|
||||
@@ -239,10 +255,12 @@ mod tests {
|
||||
"/static/js/events.js"
|
||||
);
|
||||
assert_eq!(routing_path("//admin///users").unwrap(), "/admin/users");
|
||||
assert_eq!(routing_path("/%2fadmin").unwrap(), "/admin");
|
||||
for path in [
|
||||
"/api/../admin",
|
||||
"/api/%2e%2e/admin",
|
||||
"/%2fadmin",
|
||||
"/api%2F..%2Fadmin",
|
||||
"/api%2F%2E%2E%2Fadmin",
|
||||
"/%5cadmin",
|
||||
"/%2561dmin",
|
||||
"/%00",
|
||||
@@ -349,7 +367,7 @@ mod tests {
|
||||
for peer in ["10.1.2.3", "203.0.113.5"] {
|
||||
let peer = peer.parse().unwrap();
|
||||
assert_eq!(
|
||||
allows_profiles_with_country(&[single.clone()], peer, country),
|
||||
allows_profiles_with_country(std::slice::from_ref(&single), peer, country),
|
||||
allows_with_country(single.acl.as_ref(), &single.countries, peer, country),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user