make fleet routes endpoint authoritative

This commit is contained in:
lda
2026-05-27 01:52:05 +07:00 Verified
parent 8628af6bd3
commit ba95257309
4 changed files with 228 additions and 63 deletions
@@ -59,29 +59,6 @@ CREATE TABLE agent_device_ips (
CREATE INDEX agent_device_ips_ip_idx CREATE INDEX agent_device_ips_ip_idx
ON agent_device_ips(ip); ON agent_device_ips(ip);
CREATE TABLE agent_device_endpoints (
agent_id TEXT NOT NULL,
device_key TEXT NOT NULL,
endpoint_key TEXT NOT NULL,
source TEXT NOT NULL,
mac TEXT,
ip TEXT,
hostname TEXT,
interface TEXT,
presence TEXT NOT NULL,
first_seen_unix INTEGER NOT NULL,
last_seen_unix INTEGER NOT NULL,
PRIMARY KEY (agent_id, device_key, endpoint_key),
FOREIGN KEY (agent_id, device_key)
REFERENCES agent_devices(agent_id, device_key) ON DELETE CASCADE
) WITHOUT ROWID;
CREATE INDEX agent_device_endpoints_mac_idx
ON agent_device_endpoints(mac);
CREATE INDEX agent_device_endpoints_ip_idx
ON agent_device_endpoints(ip);
CREATE TABLE agent_device_hostnames ( CREATE TABLE agent_device_hostnames (
agent_id TEXT NOT NULL, agent_id TEXT NOT NULL,
device_key TEXT NOT NULL, device_key TEXT NOT NULL,
@@ -0,0 +1,22 @@
CREATE TABLE agent_device_endpoints (
agent_id TEXT NOT NULL,
device_key TEXT NOT NULL,
endpoint_key TEXT NOT NULL,
source TEXT NOT NULL,
mac TEXT,
ip TEXT,
hostname TEXT,
interface TEXT,
presence TEXT NOT NULL,
first_seen_unix INTEGER NOT NULL,
last_seen_unix INTEGER NOT NULL,
PRIMARY KEY (agent_id, device_key, endpoint_key),
FOREIGN KEY (agent_id, device_key)
REFERENCES agent_devices(agent_id, device_key) ON DELETE CASCADE
) WITHOUT ROWID;
CREATE INDEX agent_device_endpoints_mac_idx
ON agent_device_endpoints(mac);
CREATE INDEX agent_device_endpoints_ip_idx
ON agent_device_endpoints(ip);
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::net::IpAddr; use std::net::IpAddr;
use macaddr::MacAddr; use macaddr::MacAddr;
use wakey_core::Presence; use wakey_core::{EndpointSource, Presence};
use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary}; use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary};
@@ -203,12 +203,25 @@ fn add_agent_device_to_entry(
entry.pinned = summary.pinned; entry.pinned = summary.pinned;
entry.known_device = Some(summary); entry.known_device = Some(summary);
} }
for mac in &agent_device.macs { if agent_device.endpoints.is_empty() {
entry.macs.insert(*mac); for mac in &agent_device.macs {
} entry.macs.insert(*mac);
if !device_offline { }
for ip in &agent_device.ips { if !device_offline {
entry.ips.insert(*ip); for ip in &agent_device.ips {
entry.ips.insert(*ip);
}
}
} else {
for endpoint in &agent_device.endpoints {
if let Some(mac) = endpoint.key.mac {
entry.macs.insert(mac);
}
if endpoint_ip_is_summary_eligible(endpoint.key.source, endpoint.presence)
&& let Some(ip) = endpoint.key.ip
{
entry.ips.insert(ip);
}
} }
} }
for hostname in &agent_device.hostnames { for hostname in &agent_device.hostnames {
@@ -285,11 +298,10 @@ fn add_agent_device_to_entry(
&agent_id, &agent_id,
endpoint.key.mac.as_ref(), endpoint.key.mac.as_ref(),
endpoint.key.ip.as_ref(), endpoint.key.ip.as_ref(),
&source,
); );
let wakeable = status.connected && endpoint.key.mac.is_some(); let wakeable = status.connected && endpoint.key.mac.is_some();
entry.routes.insert( insert_route(
rid.clone(), entry,
FleetWakeRoute { FleetWakeRoute {
route_id: rid, route_id: rid,
agent_id: agent_id.clone(), agent_id: agent_id.clone(),
@@ -311,10 +323,10 @@ fn add_agent_device_to_entry(
for mac in &agent_device.macs { for mac in &agent_device.macs {
let ip_for_mac = agent_device.ips.first().copied(); let ip_for_mac = agent_device.ips.first().copied();
let hostname_for_mac = agent_device.hostnames.first().cloned(); let hostname_for_mac = agent_device.hostnames.first().cloned();
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref(), "device"); let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref());
let wakeable = status.connected; let wakeable = status.connected;
entry.routes.insert( insert_route(
rid.clone(), entry,
FleetWakeRoute { FleetWakeRoute {
route_id: rid, route_id: rid,
agent_id: agent_id.clone(), agent_id: agent_id.clone(),
@@ -338,9 +350,9 @@ fn add_agent_device_to_entry(
&& let Some(ip) = agent_device.ips.first() && let Some(ip) = agent_device.ips.first()
{ {
let hostname = agent_device.hostnames.first().cloned(); let hostname = agent_device.hostnames.first().cloned();
let rid = route_id(&agent_id, None, Some(ip), "device"); let rid = route_id(&agent_id, None, Some(ip));
entry.routes.insert( insert_route(
rid.clone(), entry,
FleetWakeRoute { FleetWakeRoute {
route_id: rid, route_id: rid,
agent_id: agent_id.clone(), agent_id: agent_id.clone(),
@@ -380,19 +392,66 @@ fn device_known_device(
context: &FleetBuildContext, context: &FleetBuildContext,
) -> Option<KnownDeviceSummary> { ) -> Option<KnownDeviceSummary> {
agent_device agent_device
.macs .endpoints
.first() .iter()
.filter_map(|endpoint| endpoint.key.mac)
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase())) .map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
.and_then(|key| context.identifier_map.get(&key).cloned()) .find_map(|key| context.identifier_map.get(&key).cloned())
.or_else(|| {
agent_device
.macs
.iter()
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
.find_map(|key| context.identifier_map.get(&key).cloned())
})
.or_else(|| {
agent_device
.endpoints
.iter()
.filter_map(|endpoint| endpoint.key.ip)
.map(|ip| format!("ip:{ip}"))
.find_map(|key| context.identifier_map.get(&key).cloned())
})
.or_else(|| { .or_else(|| {
agent_device agent_device
.ips .ips
.first() .iter()
.map(|ip| format!("ip:{ip}")) .map(|ip| format!("ip:{ip}"))
.and_then(|key| context.identifier_map.get(&key).cloned()) .find_map(|key| context.identifier_map.get(&key).cloned())
}) })
} }
fn endpoint_ip_is_summary_eligible(source: EndpointSource, presence: Presence) -> bool {
match source {
EndpointSource::DhcpLease => true,
EndpointSource::Neighbor => presence != Presence::Offline,
EndpointSource::HookNeighbor | EndpointSource::HookDhcp => false,
}
}
fn insert_route(entry: &mut FleetAccumulator, candidate: FleetWakeRoute) {
match entry.routes.entry(candidate.route_id.clone()) {
std::collections::btree_map::Entry::Vacant(slot) => {
slot.insert(candidate);
}
std::collections::btree_map::Entry::Occupied(mut slot) => {
if compare_route_quality(&candidate, slot.get()).is_gt() {
slot.insert(candidate);
}
}
}
}
fn compare_route_quality(a: &FleetWakeRoute, b: &FleetWakeRoute) -> std::cmp::Ordering {
a.connected
.cmp(&b.connected)
.then_with(|| a.wakeable.cmp(&b.wakeable))
.then_with(|| a.ip.is_some().cmp(&b.ip.is_some()))
.then_with(|| a.presence.cmp(&b.presence))
.then_with(|| a.last_seen_unix.cmp(&b.last_seen_unix))
.then_with(|| source_quality_rank(&a.source).cmp(&source_quality_rank(&b.source)))
}
pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary { pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
KnownDeviceSummary { KnownDeviceSummary {
device_id: device.device_id.clone(), device_id: device.device_id.clone(),
@@ -404,16 +463,8 @@ pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
impl FleetAccumulator { impl FleetAccumulator {
fn into_response(self) -> FleetDevice { fn into_response(self) -> FleetDevice {
let mut route_candidates = self.routes.into_values().collect::<Vec<_>>(); let mut route_candidates = self.routes.into_values().collect::<Vec<_>>();
route_candidates.sort_by(|a, b| { route_candidates
b.connected .sort_by(|a, b| compare_route_quality(b, a).then_with(|| a.agent_id.cmp(&b.agent_id)));
.cmp(&a.connected)
.then_with(|| b.wakeable.cmp(&a.wakeable))
.then_with(|| b.ip.is_some().cmp(&a.ip.is_some()))
.then_with(|| b.presence.cmp(&a.presence))
.then_with(|| b.last_seen_unix.cmp(&a.last_seen_unix))
.then_with(|| source_quality_rank(&b.source).cmp(&source_quality_rank(&a.source)))
.then_with(|| a.agent_id.cmp(&b.agent_id))
});
let recommended_route = route_candidates let recommended_route = route_candidates
.iter() .iter()
.find(|route| route.wakeable) .find(|route| route.wakeable)
@@ -445,12 +496,12 @@ impl FleetAccumulator {
} }
} }
fn endpoint_source_label(source: wakey_core::EndpointSource) -> &'static str { fn endpoint_source_label(source: EndpointSource) -> &'static str {
match source { match source {
wakey_core::EndpointSource::Neighbor => "neighbor", EndpointSource::Neighbor => "neighbor",
wakey_core::EndpointSource::DhcpLease => "dhcp_lease", EndpointSource::DhcpLease => "dhcp_lease",
wakey_core::EndpointSource::HookNeighbor => "hook_neighbor", EndpointSource::HookNeighbor => "hook_neighbor",
wakey_core::EndpointSource::HookDhcp => "hook_dhcp", EndpointSource::HookDhcp => "hook_dhcp",
} }
} }
@@ -464,11 +515,10 @@ fn source_quality_rank(source: &str) -> u8 {
} }
} }
fn route_id(agent_id: &str, mac: Option<&MacAddr>, ip: Option<&IpAddr>, source: &str) -> String { fn route_id(agent_id: &str, mac: Option<&MacAddr>, ip: Option<&IpAddr>) -> String {
format!( format!(
"{}|{}|{}|{}", "{}|{}|{}",
agent_id, agent_id,
source,
mac.map(|m| m.to_string()).unwrap_or_default(), mac.map(|m| m.to_string()).unwrap_or_default(),
ip.map(|i| i.to_string()).unwrap_or_default() ip.map(|i| i.to_string()).unwrap_or_default()
) )
@@ -117,6 +117,16 @@ fn endpoint(
mac: Option<&str>, mac: Option<&str>,
ip: Option<&str>, ip: Option<&str>,
last_seen_unix: u64, last_seen_unix: u64,
) -> DeviceEndpoint {
endpoint_with_presence(source, mac, ip, Presence::LikelyOnline, last_seen_unix)
}
fn endpoint_with_presence(
source: EndpointSource,
mac: Option<&str>,
ip: Option<&str>,
presence: Presence,
last_seen_unix: u64,
) -> DeviceEndpoint { ) -> DeviceEndpoint {
DeviceEndpoint { DeviceEndpoint {
key: EndpointKey::new( key: EndpointKey::new(
@@ -127,7 +137,7 @@ fn endpoint(
.expect("endpoint key"), .expect("endpoint key"),
hostname: Some("lda".into()), hostname: Some("lda".into()),
interface: Some("br-lan".into()), interface: Some("br-lan".into()),
presence: Presence::LikelyOnline, presence,
first_seen_unix: Some(1), first_seen_unix: Some(1),
last_seen_unix: Some(last_seen_unix), last_seen_unix: Some(last_seen_unix),
} }
@@ -203,6 +213,112 @@ fn fleet_routes_use_endpoint_mac_ip_pairs() {
); );
} }
#[test]
fn fleet_summary_keeps_current_dhcp_ip_when_neighbor_endpoint_is_offline() {
let mac = "aa:bb:cc:dd:ee:ff";
let mut row = offline_agent_device(
"agent-a",
"mac:aa:bb:cc:dd:ee:ff",
Some(mac),
Some("192.168.1.99"),
20,
);
row.ips = vec![
"192.168.1.10".parse().unwrap(),
"192.168.1.99".parse().unwrap(),
];
row.endpoints = vec![
endpoint_with_presence(
EndpointSource::Neighbor,
Some(mac),
Some("192.168.1.99"),
Presence::Offline,
20,
),
endpoint_with_presence(
EndpointSource::DhcpLease,
Some(mac),
Some("192.168.1.10"),
Presence::Unknown,
20,
),
];
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
assert_eq!(devices.len(), 1);
assert_eq!(
devices[0].ips,
vec!["192.168.1.10".parse::<IpAddr>().unwrap()]
);
}
#[test]
fn known_ip_identifier_matches_endpoint_that_is_not_in_summary_ips() {
let known = KnownDevice {
device_id: "dev-1".into(),
display_name: "lda remembered".into(),
pinned: true,
created_at_unix: 1,
updated_at_unix: 1,
notes: None,
identifiers: vec![DeviceIdentifier {
identifier_key: "ip:192.168.1.99".into(),
device_id: "dev-1".into(),
kind: "ip".into(),
value: "192.168.1.99".into(),
created_at_unix: 1,
}],
};
let mut ctx = context(&["agent-a"]);
ctx.identifier_map
.insert("ip:192.168.1.99".into(), known_device_summary(&known));
let mut row = agent_device("agent-a", "ip:192.168.1.99", None, None, 20);
row.endpoints = vec![endpoint_with_presence(
EndpointSource::HookNeighbor,
None,
Some("192.168.1.99"),
Presence::Offline,
20,
)];
let devices = build_fleet_devices(vec![known], vec![row], &ctx);
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].device_key, "known:dev-1");
assert_eq!(devices[0].endpoints.len(), 1);
}
#[test]
fn equivalent_endpoint_routes_collapse_to_best_source() {
let mac = "aa:bb:cc:dd:ee:ff";
let ip = "192.168.1.2";
let mut row = agent_device("agent-a", "mac:aa:bb:cc:dd:ee:ff", Some(mac), Some(ip), 20);
row.endpoints = vec![
endpoint_with_presence(
EndpointSource::HookNeighbor,
Some(mac),
Some(ip),
Presence::LikelyOnline,
20,
),
endpoint_with_presence(
EndpointSource::Neighbor,
Some(mac),
Some(ip),
Presence::Online,
20,
),
];
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
assert_eq!(devices[0].endpoints.len(), 2);
assert_eq!(devices[0].route_candidates.len(), 1);
assert_eq!(devices[0].route_candidates[0].source, "neighbor");
assert_eq!(devices[0].route_candidates[0].presence, Presence::Online);
}
#[test] #[test]
fn known_device_with_two_macs_absorbs_both_device_groups() { fn known_device_with_two_macs_absorbs_both_device_groups() {
let known = KnownDevice { let known = KnownDevice {