Watch
0
0
Fork
You've already forked hyperhive
0

squid intercepts restricted and log agents' web traffic, named from the bound source address

This commit is contained in:
atlas 2026-11-13 05:17:01 +01:00 • committed by mara
commit 712dcf2b25
2 changed files with 1005 additions and 154 deletions

View file

@ -8,9 +8,9 @@
//! - [`ENV_ALLOWED_FILE`]: the swarm-wide names, one per line, which
//! `nix/host-modules/hive-squid.nix` reads as part of `allowed_dst`.
//! - [`ENV_AGENT_ALLOW_FILE`]: squid configuration `include`d where the base
//! list is allowed: for each of this hive's agents with entries, an `acl`
//! for its squid user, an `acl` for its names and the `http_access allow`
//! joining them, then the rules of the agents in `log` mode ([`log_rules`]).
//! list is allowed: per agent with entries, `acl`s for its users and names
//! and an `http_access allow` for the proxy port and one for the intercept
//! ports, then the rules of the agents in `log` mode ([`log_rules`]).
//!
//! Squid reads both when it parses its configuration, so the path unit in
//! `nix/host-modules/hive-squid.nix` reloads it when either changes.
@ -328,7 +328,8 @@ fn log_agents(agents: &[String], perms: &permissions::Stored) -> Vec<String> {
/// the agent is allowed never carries the mark. The mark is the last ACL of its
/// rule: squid applies `annotate_transaction` when it is reached, whatever the
/// rule decides after it. Everything in squid's configuration before the
/// include is a deny that applies to these agents as to any other.
/// include is a deny that applies to these agents as to any other. Each agent
/// has a rule for the proxy port and one for the intercept ports (`icpt`).
fn log_rules(logging: &[String]) -> String {
if logging.is_empty() {
return String::new();
@ -338,7 +339,9 @@ fn log_rules(logging: &[String]) -> String {
let _ = write!(
rules,
"acl agent_log_{agent} proxy_auth {agent}\n\
http_access allow agent_log_{agent} !allowed_dst {MARK_ACL}\n"
acl agent_log_{agent}_ix ext_user {agent}\n\
http_access allow !icpt agent_log_{agent} !allowed_dst {MARK_ACL}\n\
http_access allow icpt agent_log_{agent}_ix !allowed_dst {MARK_ACL}\n"
);
}
rules
@ -393,8 +396,10 @@ fn render(
agent_count += names.len();
agent_rules.push(format!(
"acl agent_allow_{agent} proxy_auth {agent}\n\
acl agent_allow_{agent}_ix ext_user {agent}\n\
acl agent_allow_{agent}_dst dstdomain {}\n\
http_access allow agent_allow_{agent} agent_allow_{agent}_dst\n",
http_access allow !icpt agent_allow_{agent} agent_allow_{agent}_dst\n\
http_access allow icpt agent_allow_{agent}_ix agent_allow_{agent}_dst\n",
names.join(" ")
));
}
@ -482,8 +487,10 @@ mod tests {
r.agent_allow,
"# source: swarm\n\
acl agent_allow_atlas proxy_auth atlas\n\
acl agent_allow_atlas_ix ext_user atlas\n\
acl agent_allow_atlas_dst dstdomain crates.io pypi.org\n\
http_access allow agent_allow_atlas agent_allow_atlas_dst\n"
http_access allow !icpt agent_allow_atlas agent_allow_atlas_dst\n\
http_access allow icpt agent_allow_atlas_ix agent_allow_atlas_dst\n"
);
let counts = r.counts.expect("a swarm list");
assert_eq!((counts.swarm, counts.agent), (0, 2));
@ -530,8 +537,10 @@ mod tests {
r.agent_allow,
"# source: swarm\n\
acl agent_allow_atlas proxy_auth atlas\n\
acl agent_allow_atlas_ix ext_user atlas\n\
acl agent_allow_atlas_dst dstdomain crates.io\n\
http_access allow agent_allow_atlas agent_allow_atlas_dst\n"
http_access allow !icpt agent_allow_atlas agent_allow_atlas_dst\n\
http_access allow icpt agent_allow_atlas_ix agent_allow_atlas_dst\n"
);
// Control: the same entries survive when nothing is always allowed.
let bare = render(
@ -586,19 +595,23 @@ mod tests {
text,
"# source: swarm\n\
acl agent_allow_atlas proxy_auth atlas\n\
acl agent_allow_atlas_ix ext_user atlas\n\
acl agent_allow_atlas_dst dstdomain pypi.org\n\
http_access allow agent_allow_atlas agent_allow_atlas_dst\n\
http_access allow !icpt agent_allow_atlas agent_allow_atlas_dst\n\
http_access allow icpt agent_allow_atlas_ix agent_allow_atlas_dst\n\
acl log_mark annotate_transaction would_deny=1\n\
acl agent_log_uma proxy_auth uma\n\
http_access allow agent_log_uma !allowed_dst log_mark\n"
acl agent_log_uma_ix ext_user uma\n\
http_access allow !icpt agent_log_uma !allowed_dst log_mark\n\
http_access allow icpt agent_log_uma_ix !allowed_dst log_mark\n"
);
// The rule of an agent with its own names also follows them.
let both = log_list(&["atlas"]);
let own = both
.find("http_access allow agent_allow_atlas ")
.find("http_access allow !icpt agent_allow_atlas ")
.expect("own rule");
let log = both
.find("http_access allow agent_log_atlas ")
.find("http_access allow !icpt agent_log_atlas ")
.expect("log rule");
assert!(own < log, "{both}");
// Control: with no log agent there is no rule and no mark.
@ -616,9 +629,10 @@ mod tests {
let text = log_list(&["atlas", "iris", "uma"]);
let rules: Vec<&str> = text
.lines()
.filter(|l| l.starts_with("http_access allow agent_log_"))
.filter(|l| l.starts_with("http_access allow ") && l.contains(" agent_log_"))
.collect();
assert_eq!(rules.len(), 3, "{text}");
// One rule for the proxy port and one for the intercept ports per agent.
assert_eq!(rules.len(), 6, "{text}");
for rule in rules {
assert!(rule.ends_with(" !allowed_dst log_mark"), "{rule}");
}
@ -698,7 +712,8 @@ mod tests {
.expect("renders");
let text = text(&d.agent_allow());
assert!(text.starts_with(SEED_MARKER), "{text}");
assert!(text.contains("http_access allow agent_log_uma !allowed_dst log_mark"));
assert!(text.contains("http_access allow !icpt agent_log_uma !allowed_dst log_mark"));
assert!(text.contains("http_access allow icpt agent_log_uma_ix !allowed_dst log_mark"));
}
struct Dir(tempfile::TempDir);
@ -786,8 +801,10 @@ mod tests {
text(&d.agent_allow()),
"# source: swarm\n\
acl agent_allow_atlas proxy_auth atlas\n\
acl agent_allow_atlas_ix ext_user atlas\n\
acl agent_allow_atlas_dst dstdomain pypi.org\n\
http_access allow agent_allow_atlas agent_allow_atlas_dst\n"
http_access allow !icpt agent_allow_atlas agent_allow_atlas_dst\n\
http_access allow icpt agent_allow_atlas_ix agent_allow_atlas_dst\n"
);
}

View file

@ -169,6 +169,12 @@ struct Rendered {
/// The destination ACL comes first in each rule: squid checks ACLs left to
/// right, so a swarm-wide rule never reaches a `proxy_auth` ACL, which would
/// answer an anonymous client 407 instead of the 403.
///
/// A per-agent name gets two rules. For a request through the proxy port the
/// agent is the one whose credential came with it; `!icpt` comes first, since a
/// `proxy_auth` ACL evaluated on an intercepted request only logs a warning.
/// For an intercepted request the agent is the user the `hive_agent` helper
/// named (`ext_user`).
fn render(list: &DenyList, agents: &[String]) -> Rendered {
let mut skipped = Vec::new();
let mut swarm_wide = Vec::new();
@ -217,7 +223,9 @@ fn render(list: &DenyList, agents: &[String]) -> Rendered {
rules,
"acl deny_agent_{agent}_dst dstdomain {}\n\
acl deny_agent_{agent} proxy_auth {agent}\n\
http_access deny deny_agent_{agent}_dst deny_agent_{agent} {MARK_ACL}\n",
acl deny_agent_{agent}_ix ext_user {agent}\n\
http_access deny deny_agent_{agent}_dst !icpt deny_agent_{agent} {MARK_ACL}\n\
http_access deny deny_agent_{agent}_dst icpt deny_agent_{agent}_ix {MARK_ACL}\n",
names.join(" ")
);
}
@ -292,7 +300,9 @@ mod tests {
format!(
"{MARK_LINE}acl deny_agent_atlas_dst dstdomain crates.io pypi.org\n\
acl deny_agent_atlas proxy_auth atlas\n\
http_access deny deny_agent_atlas_dst deny_agent_atlas deny_hit_mark\n"
acl deny_agent_atlas_ix ext_user atlas\n\
http_access deny deny_agent_atlas_dst !icpt deny_agent_atlas deny_hit_mark\n\
http_access deny deny_agent_atlas_dst icpt deny_agent_atlas_ix deny_hit_mark\n"
)
);
assert_eq!((r.swarm, r.agent), (0, 2));