Watch
0
0
Fork
You've already forked hyperhive
0

always include a hive's own allowedDomains in the rendered swarm-wide list

This commit is contained in:
atlas 2026-10-29 01:47:25 +01:00 • committed by mara
commit 7e2f4ce7c8

View file

@ -15,19 +15,19 @@
//! Squid reads both when it parses its configuration, so the path unit in
//! `nix/host-modules/hive-squid.nix` reloads it when either changes.
//!
//! The swarm's list replaces the names `services.hyperhive.squid.allowedDomains`
//! lists ([`ENV_SEED_FILE`]), which stay the list until the swarm has one. A
//! file rendered from the swarm's list starts with [`SWARM_MARKER`]. The queue
//! not answering for the bucket does not show that the swarm withdrew its list,
//! so a file with the marker is left alone then.
//! The names `services.hyperhive.squid.allowedDomains` lists ([`ENV_SEED_FILE`])
//! are always allowed: the swarm-wide names written are those plus the swarm's,
//! with the per-agent rules on top. Until the swarm has a list the files hold
//! just those names. A file from the swarm's list starts with [`SWARM_MARKER`];
//! the queue not answering for the bucket is no withdrawal, so a file with the
//! marker is left alone then.
//!
//! Every entry is checked again with
//! [`swarm_wire::egress_allow::normalize_domain`], since the names end up in
//! squid's configuration, and skipped when it fails. An entry that
//! [`ENV_ALWAYS_ALLOWED`] or another entry of its list covers is dropped,
//! because squid ignores the narrower of two overlapping names in one ACL.
//!
//! A failed read leaves both files as they are.
//! because squid ignores the narrower of two overlapping names in one ACL. A
//! failed read leaves both files as they are.
use std::path::{Path, PathBuf};
use std::time::Duration;
@ -54,7 +54,7 @@ pub const ENV_ALLOWED_FILE: &str = "HIVE_SQUID_ALLOWED_FILE";
pub const ENV_AGENT_ALLOW_FILE: &str = "HIVE_SQUID_AGENT_ALLOW_FILE";
/// The file of names `services.hyperhive.squid.allowedDomains` lists, one per
/// line, which the files hold until the swarm has a list.
/// line, which every rendering of the swarm-wide names includes.
pub const ENV_SEED_FILE: &str = "HIVE_SQUID_ALLOWED_SEED_FILE";
/// The names, separated by spaces, that squid allows every authenticated user
@ -191,7 +191,7 @@ fn sync_with(
) -> Result<Source> {
let always: Vec<&str> = always.split_whitespace().collect();
let rendered = match read {
Read::Current(list) => render(&list, agents, &always),
Read::Current(list) => render(&list, agents, &always, &seed_names(seed)?),
Read::NotSet => {
if super::squid_users::read_or_empty(files.allowed)?.starts_with(SWARM_MARKER) {
return Ok(Source::Kept);
@ -251,18 +251,27 @@ struct Rendered {
counts: Option<Counts>,
}
/// The files for the seed: its names as they are, and no per-agent rules.
fn seeded(seed: Option<&Path>) -> Result<Rendered> {
let names = match seed {
/// The names in the seed file, trimmed, without blank lines and comments. The
/// file is this hive's own configuration, so its names are not re-checked.
fn seed_names(seed: Option<&Path>) -> Result<Vec<String>> {
let text = match seed {
Some(path) => super::squid_users::read_or_empty(path)?,
None => String::new(),
};
Ok(text
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(str::to_owned)
.collect())
}
/// The files for the seed: its names as they are, and no per-agent rules.
fn seeded(seed: Option<&Path>) -> Result<Rendered> {
let mut allowed = SEED_MARKER.to_owned();
for line in names.lines().map(str::trim) {
if !line.is_empty() && !line.starts_with('#') {
allowed.push_str(line);
allowed.push('\n');
}
for name in seed_names(seed)? {
allowed.push_str(&name);
allowed.push('\n');
}
Ok(Rendered {
allowed,
@ -283,8 +292,9 @@ fn reduced(mut names: Vec<String>) -> Vec<String> {
names
}
/// The files for `list`, for the agents this hive runs.
fn render(list: &AllowList, agents: &[String], always: &[&str]) -> Rendered {
/// The files for `list` and the seed's `own` names, for the agents this hive
/// runs.
fn render(list: &AllowList, agents: &[String], always: &[&str], own: &[String]) -> Rendered {
let mut skipped = Vec::new();
let mut swarm_wide = Vec::new();
let mut per_agent: std::collections::BTreeMap<&str, Vec<String>> =
@ -305,6 +315,7 @@ fn render(list: &AllowList, agents: &[String], always: &[&str]) -> Rendered {
}
}
}
swarm_wide.extend(own.iter().map(|name| name.to_ascii_lowercase()));
swarm_wide.retain(|d| !covered_by(d, always));
let swarm_wide = reduced(swarm_wide);
@ -366,7 +377,7 @@ mod tests {
const ALWAYS: &[&str] = &["api.anthropic.com", ".claude.ai", "cache.nixos.org"];
fn rendered(entries: &[(&str, Option<&str>)], mine: &[&str]) -> Rendered {
render(&list(entries), &agents(mine), ALWAYS)
render(&list(entries), &agents(mine), ALWAYS, &[])
}
#[test]
@ -464,6 +475,7 @@ mod tests {
&list(&[("api.anthropic.com", None), ("a.claude.ai", None)]),
&agents(&["atlas"]),
&[],
&[],
);
assert_eq!(
bare.allowed,
@ -545,18 +557,74 @@ mod tests {
);
}
/// The seed is the hive's own additive list: a swarm list, empty or not,
/// renders the seed's names with it.
#[test]
fn a_swarm_list_replaces_the_seed_even_when_empty() {
fn a_swarm_list_adds_to_the_seed_even_when_empty() {
let d = dir("forge.example\n");
sync(&d, Read::NotSet).expect("seeded");
assert!(text(&d.allowed()).contains("forge.example"), "control");
let empty = Read::Current(AllowList::default());
assert_eq!(sync(&d, empty).expect("renders"), Source::Swarm);
assert_eq!(text(&d.allowed()), SWARM_MARKER);
let named = Read::Current(list(&[("github.com", None), ("pypi.org", Some("atlas"))]));
assert_eq!(text(&d.allowed()), "# source: swarm\nforge.example\n");
let named = Read::Current(list(&[("github.com", None)]));
sync(&d, named).expect("renders");
assert_eq!(
text(&d.allowed()),
"# source: swarm\nforge.example\ngithub.com\n"
);
assert_eq!(text(&d.agent_allow()), SWARM_MARKER);
}
#[test]
fn a_per_agent_entry_renders_on_top_of_the_seed() {
let d = dir("forge.example\n");
let read = Read::Current(list(&[("pypi.org", Some("atlas"))]));
sync(&d, read).expect("renders");
assert_eq!(text(&d.allowed()), "# source: swarm\nforge.example\n");
assert_eq!(
text(&d.agent_allow()),
"# source: swarm\n\
acl agent_allow_atlas proxy_auth atlas\n\
acl agent_allow_atlas_dst dstdomain pypi.org\n\
http_access allow agent_allow_atlas agent_allow_atlas_dst\n"
);
}
#[test]
fn a_seed_name_the_swarm_repeats_or_squid_allows_anyway_is_written_once() {
// `cache.nixos.org` is always allowed; `.example.com` covers the
// swarm's `api.example.com`.
let d = dir("Forge.Example\n.example.com\ncache.nixos.org\n");
let read = Read::Current(list(&[
("forge.example", None),
("api.example.com", None),
("github.com", None),
]));
sync(&d, read).expect("renders");
assert_eq!(
text(&d.allowed()),
"# source: swarm\n.example.com\nforge.example\ngithub.com\n"
);
}
#[test]
fn no_seed_file_is_no_seed_names() {
let d = dir("");
std::fs::remove_file(d.seed()).expect("remove");
let read = Read::Current(list(&[("github.com", None)]));
sync_with(
read,
&agents(&["atlas"]),
None,
"",
&Files {
allowed: &d.allowed(),
agent_allow: &d.agent_allow(),
},
)
.expect("renders");
assert_eq!(text(&d.allowed()), "# source: swarm\ngithub.com\n");
assert!(text(&d.agent_allow()).contains("acl agent_allow_atlas proxy_auth atlas"));
}
#[test]