use std::net::SocketAddr;
use serde::Serialize;
use crate::connect::acl::ConnectPolicy;
use crate::identity::{AgentId, MachineId};
use crate::trust::TrustDecision;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConnectDenialReason {
UnverifiedSender,
TrustRejected,
ConnectDisabled,
TargetNotLoopback,
AgentMachineNotInAcl,
TargetNotAllowed,
AttestationFailed,
AgentNotOnMachine,
}
pub fn evaluate_connect_gate(
verified: bool,
trust_decision: Option<TrustDecision>,
policy: &ConnectPolicy,
agent_id: &AgentId,
machine_id: &MachineId,
target: &SocketAddr,
) -> Result<(), ConnectDenialReason> {
if !verified {
return Err(ConnectDenialReason::UnverifiedSender);
}
if trust_decision != Some(TrustDecision::Accept) {
return Err(ConnectDenialReason::TrustRejected);
}
let ConnectPolicy::Enabled(acl) = policy else {
return Err(ConnectDenialReason::ConnectDisabled);
};
if !crate::connect::acl::is_loopback(target.ip()) {
return Err(ConnectDenialReason::TargetNotLoopback);
}
if !acl.is_allowed(agent_id, machine_id, target) {
return Err(if acl.entry_for(agent_id, machine_id).is_some() {
ConnectDenialReason::TargetNotAllowed
} else {
ConnectDenialReason::AgentMachineNotInAcl
});
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::connect::acl::{ConnectAcl, ConnectAllowEntry, ConnectPolicy};
use crate::exec::acl::{parse_agent_id, parse_machine_id};
use crate::trust::TrustDecision;
use std::net::SocketAddr;
use std::path::Path;
fn enabled_acl_with(agent: &str, machine: &str, targets: &[&str]) -> ConnectPolicy {
let agent_id = parse_agent_id(&agent.repeat(32)).unwrap();
let machine_id = parse_machine_id(&machine.repeat(32)).unwrap();
let targets: Vec<SocketAddr> = targets.iter().map(|t| t.parse().unwrap()).collect();
ConnectPolicy::Enabled(ConnectAcl {
loaded_from: Path::new("/tmp/x").to_path_buf(),
loaded_at_unix_ms: 0,
allow: vec![ConnectAllowEntry {
description: None,
agent_id,
machine_id,
targets,
}],
})
}
fn pair() -> (crate::identity::AgentId, crate::identity::MachineId) {
(
parse_agent_id(&"ab".repeat(32)).unwrap(),
parse_machine_id(&"cd".repeat(32)).unwrap(),
)
}
const T22: &str = "127.0.0.1:22";
#[test]
fn gate_denies_unverified_sender_before_policy() {
let (a, m) = pair();
let policy = enabled_acl_with("ab", "cd", &[T22]);
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(false, Some(TrustDecision::Accept), &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::UnverifiedSender));
}
#[test]
fn gate_denies_non_accept_trust_decision() {
let (a, m) = pair();
let policy = enabled_acl_with("ab", "cd", &[T22]);
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(
true,
Some(TrustDecision::AcceptWithFlag),
&policy,
&a,
&m,
&target,
);
assert_eq!(r, Err(ConnectDenialReason::TrustRejected));
}
#[test]
fn gate_denies_verified_sender_with_no_trust_decision() {
let (a, m) = pair();
let policy = enabled_acl_with("ab", "cd", &[T22]);
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(true, None, &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::TrustRejected));
}
#[test]
fn gate_denies_when_policy_disabled() {
let (a, m) = pair();
let policy = ConnectPolicy::Disabled {
path: Path::new("/tmp/x").to_path_buf(),
reason: "test".to_string(),
loaded_at_unix_ms: 0,
};
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(true, Some(TrustDecision::Accept), &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::ConnectDisabled));
}
#[test]
fn gate_denies_non_loopback_requested_target() {
let (a, m) = pair();
let policy = enabled_acl_with("ab", "cd", &["10.0.0.1:22"]); let target: SocketAddr = "10.0.0.1:22".parse().unwrap();
let r = evaluate_connect_gate(true, Some(TrustDecision::Accept), &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::TargetNotLoopback));
}
#[test]
fn gate_denies_pair_not_in_acl() {
let (a, m) = pair();
let policy = enabled_acl_with("ff", "ee", &[T22]);
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(true, Some(TrustDecision::Accept), &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::AgentMachineNotInAcl));
}
#[test]
fn gate_denies_listed_pair_wrong_target() {
let (a, m) = pair();
let policy = enabled_acl_with("ab", "cd", &[T22]);
let target: SocketAddr = "127.0.0.1:23".parse().unwrap(); let r = evaluate_connect_gate(true, Some(TrustDecision::Accept), &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::TargetNotAllowed));
}
#[test]
fn gate_denies_wrong_machine_same_agent() {
let (a, _m) = pair();
let wrong_machine = parse_machine_id(&"99".repeat(32)).unwrap();
let policy = enabled_acl_with("ab", "cd", &[T22]);
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(
true,
Some(TrustDecision::Accept),
&policy,
&a,
&wrong_machine,
&target,
);
assert_eq!(r, Err(ConnectDenialReason::AgentMachineNotInAcl));
}
#[test]
fn gate_v4_and_v6_loopback_are_distinct_grants() {
let (a, m) = pair();
let policy = enabled_acl_with("ab", "cd", &["127.0.0.1:22"]);
let v6: SocketAddr = "[::1]:22".parse().unwrap();
let r = evaluate_connect_gate(true, Some(TrustDecision::Accept), &policy, &a, &m, &v6);
assert_eq!(r, Err(ConnectDenialReason::TargetNotAllowed));
}
#[test]
fn gate_allows_exact_triple() {
let (a, m) = pair();
let policy = enabled_acl_with("ab", "cd", &[T22]);
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(true, Some(TrustDecision::Accept), &policy, &a, &m, &target);
assert!(r.is_ok());
}
#[test]
fn gate_order_unverified_beats_trust_and_disabled() {
let (a, m) = pair();
let policy = ConnectPolicy::default(); let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(false, None, &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::UnverifiedSender));
}
#[test]
fn gate_order_trust_beats_disabled() {
let (a, m) = pair();
let policy = ConnectPolicy::default();
let target: SocketAddr = T22.parse().unwrap();
let r = evaluate_connect_gate(true, None, &policy, &a, &m, &target);
assert_eq!(r, Err(ConnectDenialReason::TrustRejected));
}
}