Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 61 additions & 9 deletions devolutions-gateway/src/api/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,37 @@ pub struct EnrollResponse {

#[derive(Serialize)]
pub struct AgentDomainAdvertisement {
/// Domain route advertised by the Agent.
pub domain: String,
/// Whether the Agent discovered the domain automatically.
pub auto_detected: bool,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentStatus {
/// No tunnel connection exists for the Agent.
Offline,
/// The Agent has a tunnel connection and a recent heartbeat.
Online,
/// The Agent has a tunnel connection, but its heartbeat has expired.
Unresponsive,
}

#[derive(Serialize)]
pub struct AgentInfo {
/// Stable Agent identity.
pub agent_id: Uuid,
/// Unique management name assigned during enrollment.
pub name: String,
pub cert_fingerprint: Option<String>,
pub is_online: bool,
/// Current tunnel connection status.
pub status: AgentStatus,
/// Last heartbeat timestamp in milliseconds since the Unix epoch.
pub last_seen_ms: Option<u64>,
/// Subnet routes currently advertised by the Agent.
pub subnets: Option<Vec<String>>,
/// Domain routes currently advertised by the Agent.
pub domains: Option<Vec<AgentDomainAdvertisement>>,
pub route_epoch: Option<u64>,
}

pub fn make_router<S>(state: DgwState) -> Router<S> {
Expand Down Expand Up @@ -194,20 +211,21 @@ fn agent_info(
return AgentInfo {
agent_id: accepted.agent_id,
name: accepted.name,
cert_fingerprint: None,
is_online: false,
status: AgentStatus::Offline,
last_seen_ms: None,
subnets: None,
domains: None,
route_epoch: None,
};
};

AgentInfo {
agent_id: accepted.agent_id,
name: accepted.name,
cert_fingerprint: Some(runtime.cert_fingerprint),
is_online: runtime.is_online,
status: if runtime.is_online {
AgentStatus::Online
} else {
AgentStatus::Unresponsive
},
last_seen_ms: Some(runtime.last_seen_ms),
subnets: Some(runtime.subnets),
domains: Some(
Expand All @@ -220,7 +238,6 @@ fn agent_info(
})
.collect(),
),
route_epoch: Some(runtime.route_epoch),
}
}

Expand Down Expand Up @@ -292,3 +309,38 @@ async fn delete_agent(

Ok(axum::http::StatusCode::NO_CONTENT)
}

#[cfg(test)]
mod tests {
use super::*;

fn accepted_agent(agent_id: Uuid) -> agent_tunnel::authorization::AcceptedAgent {
agent_tunnel::authorization::AcceptedAgent {
agent_id,
name: String::from("montreal-office"),
client_spki_sha256: [0x11; 32],
}
}

#[test]
fn connected_agent_without_recent_heartbeat_is_unresponsive() {
let agent_id = Uuid::new_v4();
let runtime = agent_tunnel::registry::AgentInfo {
agent_id,
name: String::from("montreal-office"),
cert_fingerprint: String::from("fingerprint"),
is_online: false,
last_seen_ms: 1234,
subnets: Vec::new(),
domains: Vec::new(),
route_epoch: 1,
};

let info = agent_info(accepted_agent(agent_id), Some(runtime));

assert_eq!(info.status, AgentStatus::Unresponsive);
assert_eq!(info.last_seen_ms, Some(1234));
assert_eq!(info.subnets, Some(Vec::new()));
assert!(info.domains.is_some_and(|domains| domains.is_empty()));
}
}
11 changes: 5 additions & 6 deletions testsuite/tests/cli/agent/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,7 @@ async fn wait_for_registered_agent(
})
});

if agent.get("is_online").and_then(serde_json::Value::as_bool) == Some(true)
&& agent.get("route_epoch").and_then(serde_json::Value::as_u64) == Some(1)
if agent.get("status").and_then(serde_json::Value::as_str) == Some("online")
&& subnets == expected_subnets
&& domains == expected_domains
&& domains_are_explicit
Expand Down Expand Up @@ -680,14 +679,14 @@ async fn enrolled_agent_forwards_domain_only_route_and_reconnects() {
Some("smoke-agent")
);
assert_eq!(
offline.get("is_online").and_then(serde_json::Value::as_bool),
Some(false)
offline.get("status").and_then(serde_json::Value::as_str),
Some("offline")
);
assert!(offline.get("cert_fingerprint").is_some_and(serde_json::Value::is_null));
assert!(offline.get("last_seen_ms").is_some_and(serde_json::Value::is_null));
assert!(offline.get("subnets").is_some_and(serde_json::Value::is_null));
assert!(offline.get("domains").is_some_and(serde_json::Value::is_null));
assert!(offline.get("route_epoch").is_some_and(serde_json::Value::is_null));
assert!(offline.get("cert_fingerprint").is_none());
assert!(offline.get("route_epoch").is_none());
let mut agent = agent_tokio_cmd()
.env("DAGENT_CONFIG_PATH", agent_config.path())
.arg("run")
Expand Down
Loading