use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::collections::HashMap;
use crate::ast::{Direction, Protocol};
use super::DissectedPacket;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DiscoveredHost {
pub name: String,
pub ip: Option<IpAddr>,
pub mac: Option<[u8; 6]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ResolvedFrameEndpoints {
pub caller_idx: usize,
pub callee_idx: usize,
pub dir: Direction,
pub emit_src_mac: Option<[u8; 6]>,
pub emit_dst_mac: Option<[u8; 6]>,
}
#[derive(Debug, Clone)]
pub(crate) struct CaptureTopology {
pub hosts: Vec<DiscoveredHost>,
pub frame_endpoints: Vec<ResolvedFrameEndpoints>,
}
#[derive(Debug)]
struct IpHostAccumulator {
mac_weights: Vec<([u8; 6], u32)>,
all_l3_only: bool,
}
impl Default for IpHostAccumulator {
fn default() -> Self {
Self {
mac_weights: Vec::new(),
all_l3_only: true,
}
}
}
impl IpHostAccumulator {
fn record(&mut self, mac: [u8; 6], is_l3_only: bool, weight: u32) {
if !is_l3_only {
self.all_l3_only = false;
if is_unicast_mac(mac) {
if let Some(entry) = self.mac_weights.iter_mut().find(|(m, _)| *m == mac) {
entry.1 = entry.1.saturating_add(weight);
} else {
self.mac_weights.push((mac, weight));
}
}
}
}
fn dominant_mac(&self) -> Option<[u8; 6]> {
if self.all_l3_only {
return None;
}
self.mac_weights
.iter()
.max_by_key(|(_, w)| *w)
.map(|(m, _)| *m)
}
}
pub(crate) fn is_unicast_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => !v4.is_unspecified() && !v4.is_broadcast() && !v4.is_multicast(),
IpAddr::V6(v6) => !v6.is_unspecified() && !v6.is_multicast(),
}
}
pub(crate) fn is_unicast_mac(mac: [u8; 6]) -> bool {
mac != [0, 0, 0, 0, 0, 0] && (mac[0] & 0x01) == 0
}
pub(crate) fn derive_mac(
host_mac: Option<[u8; 6]>,
stmt_mac: Option<[u8; 6]>,
ip: Option<IpAddr>,
is_caller: bool,
) -> [u8; 6] {
if let Some(mac) = stmt_mac {
return mac;
}
if let Some(mac) = host_mac {
return mac;
}
match ip {
Some(IpAddr::V6(v6)) => {
let octets = v6.octets();
if octets[0] == 0xFF {
[0x33, 0x33, octets[12], octets[13], octets[14], octets[15]]
} else if is_caller {
[0x00, 0x00, 0x00, 0x00, 0x00, 0x01]
} else {
[0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
}
}
Some(IpAddr::V4(v4)) => {
let octets = v4.octets();
if (224..=239).contains(&octets[0]) {
[0x01, 0x00, 0x5E, octets[1] & 0x7F, octets[2], octets[3]]
} else if is_caller {
[0x00, 0x00, 0x00, 0x00, 0x00, 0x01]
} else {
[0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
}
}
None => {
if is_caller {
[0x00, 0x00, 0x00, 0x00, 0x00, 0x01]
} else {
[0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
}
}
}
}
fn predict_synthesized_macs(
pkt: &DissectedPacket,
caller_host: &DiscoveredHost,
callee_host: &DiscoveredHost,
dir: Direction,
) -> ([u8; 6], [u8; 6]) {
let (src_host, dst_host, is_forward) = match dir {
Direction::LeftToRight => (caller_host, callee_host, true),
Direction::RightToLeft => (callee_host, caller_host, false),
};
let synth_src = derive_mac(src_host.mac, None, src_host.ip, is_forward);
let synth_dst = match pkt.dissection.protocol {
Protocol::Lldp => [0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e],
Protocol::Stp => [0x01, 0x80, 0xc2, 0x00, 0x00, 0x00],
Protocol::Lacp => [0x01, 0x80, 0xc2, 0x00, 0x00, 0x02],
Protocol::Eapol => [0x01, 0x80, 0xc2, 0x00, 0x00, 0x03],
Protocol::Arp => {
let is_reply = pkt
.dissection
.tokens
.split_whitespace()
.any(|tok| tok == "reply");
if is_reply {
derive_mac(dst_host.mac, None, dst_host.ip, !is_forward)
} else {
[0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
}
}
Protocol::Ndp
| Protocol::Igmp
| Protocol::Mld
| Protocol::Ospf
| Protocol::Rip
| Protocol::Vrrp
| Protocol::Hsrp
| Protocol::Pim
| Protocol::Mdns => derive_mac(None, None, pkt.dst_ip, false),
Protocol::Ssdp => {
let is_notify = pkt
.dissection
.tokens
.split_whitespace()
.any(|tok| tok == "notify");
let is_msearch = !pkt
.dissection
.tokens
.split_whitespace()
.any(|tok| tok == "response");
if is_notify || is_msearch {
derive_mac(None, None, pkt.dst_ip, false)
} else {
derive_mac(dst_host.mac, None, dst_host.ip, !is_forward)
}
}
_ => derive_mac(dst_host.mac, None, dst_host.ip, !is_forward),
};
(synth_src, synth_dst)
}
pub(crate) fn build_topology(packets: &[DissectedPacket]) -> CaptureTopology {
if packets.is_empty() {
return CaptureTopology {
hosts: Vec::new(),
frame_endpoints: Vec::new(),
};
}
let mut hosts: Vec<DiscoveredHost> = Vec::new();
let mut ip_to_host: HashMap<IpAddr, usize> = HashMap::new();
let mut ip_accumulators: HashMap<usize, IpHostAccumulator> = HashMap::new();
let mut mac_to_l2_host: HashMap<[u8; 6], usize> = HashMap::new();
for pkt in packets {
let is_ip_domain = pkt.has_ip_header || pkt.dissection.protocol == Protocol::Arp;
if is_ip_domain {
if let Some(sip) = pkt.src_ip
&& is_unicast_ip(sip)
{
let idx = *ip_to_host.entry(sip).or_insert_with(|| {
let new_idx = hosts.len();
hosts.push(DiscoveredHost {
name: format!("host_{}", new_idx + 1),
ip: Some(sip),
mac: None,
});
new_idx
});
ip_accumulators
.entry(idx)
.or_default()
.record(pkt.src_mac, pkt.is_l3_only, 2);
}
if let Some(dip) = pkt.dst_ip
&& is_unicast_ip(dip)
{
let idx = *ip_to_host.entry(dip).or_insert_with(|| {
let new_idx = hosts.len();
hosts.push(DiscoveredHost {
name: format!("host_{}", new_idx + 1),
ip: Some(dip),
mac: None,
});
new_idx
});
ip_accumulators
.entry(idx)
.or_default()
.record(pkt.dst_mac, pkt.is_l3_only, 1);
}
} else {
if is_unicast_mac(pkt.src_mac) {
mac_to_l2_host.entry(pkt.src_mac).or_insert_with(|| {
let new_idx = hosts.len();
hosts.push(DiscoveredHost {
name: format!("host_{}", new_idx + 1),
ip: None,
mac: Some(pkt.src_mac),
});
new_idx
});
}
if is_unicast_mac(pkt.dst_mac) {
mac_to_l2_host.entry(pkt.dst_mac).or_insert_with(|| {
let new_idx = hosts.len();
hosts.push(DiscoveredHost {
name: format!("host_{}", new_idx + 1),
ip: None,
mac: Some(pkt.dst_mac),
});
new_idx
});
}
}
}
for (idx, acc) in &ip_accumulators {
if let Some(host) = hosts.get_mut(*idx) {
host.mac = acc.dominant_mac();
}
}
let mut pair_initiators: HashMap<(usize, usize), (usize, usize)> = HashMap::new();
let mut frame_endpoints = Vec::with_capacity(packets.len());
for pkt in packets {
let is_ip_domain = pkt.has_ip_header || pkt.dissection.protocol == Protocol::Arp;
let (src_idx, dst_idx) = if is_ip_domain {
let is_v6 = matches!(pkt.src_ip, Some(IpAddr::V6(_)))
|| matches!(pkt.dst_ip, Some(IpAddr::V6(_)));
let s_opt = pkt
.src_ip
.filter(|&ip| is_unicast_ip(ip))
.and_then(|ip| ip_to_host.get(&ip).copied());
let d_opt = pkt
.dst_ip
.filter(|&ip| is_unicast_ip(ip))
.and_then(|ip| ip_to_host.get(&ip).copied());
match (s_opt, d_opt) {
(Some(s), Some(d)) if s != d => (s, d),
(Some(s), _) => {
let d = find_or_create_ip_peer(
&mut hosts,
&mut ip_to_host,
Some(s),
is_v6,
pkt.dst_mac,
pkt.is_l3_only,
);
(s, d)
}
(None, Some(d)) => {
let s = find_or_create_ip_peer(
&mut hosts,
&mut ip_to_host,
Some(d),
is_v6,
pkt.src_mac,
pkt.is_l3_only,
);
(s, d)
}
(None, None) => {
let s = find_or_create_ip_peer(
&mut hosts,
&mut ip_to_host,
None,
is_v6,
pkt.src_mac,
pkt.is_l3_only,
);
let d = find_or_create_ip_peer(
&mut hosts,
&mut ip_to_host,
Some(s),
is_v6,
pkt.dst_mac,
pkt.is_l3_only,
);
(s, d)
}
}
} else {
let s_opt = is_unicast_mac(pkt.src_mac)
.then(|| mac_to_l2_host.get(&pkt.src_mac).copied())
.flatten();
let d_opt = is_unicast_mac(pkt.dst_mac)
.then(|| mac_to_l2_host.get(&pkt.dst_mac).copied())
.flatten();
match (s_opt, d_opt) {
(Some(s), Some(d)) if s != d => (s, d),
(Some(s), _) => {
let d = find_or_create_l2_peer(&mut hosts, &mut mac_to_l2_host, Some(s));
(s, d)
}
(None, Some(d)) => {
let s = find_or_create_l2_peer(&mut hosts, &mut mac_to_l2_host, Some(d));
(s, d)
}
(None, None) => {
let s = find_or_create_l2_peer(&mut hosts, &mut mac_to_l2_host, None);
let d = find_or_create_l2_peer(&mut hosts, &mut mac_to_l2_host, Some(s));
(s, d)
}
}
};
let unordered_key = if src_idx <= dst_idx {
(src_idx, dst_idx)
} else {
(dst_idx, src_idx)
};
let &mut (caller_idx, callee_idx) = pair_initiators
.entry(unordered_key)
.or_insert((src_idx, dst_idx));
let dir = if src_idx == caller_idx {
Direction::LeftToRight
} else {
Direction::RightToLeft
};
let (synth_src, synth_dst) =
predict_synthesized_macs(pkt, &hosts[caller_idx], &hosts[callee_idx], dir);
let emit_src_mac = (pkt.src_mac != synth_src).then_some(pkt.src_mac);
let emit_dst_mac = (pkt.dst_mac != synth_dst).then_some(pkt.dst_mac);
frame_endpoints.push(ResolvedFrameEndpoints {
caller_idx,
callee_idx,
dir,
emit_src_mac,
emit_dst_mac,
});
}
CaptureTopology {
hosts,
frame_endpoints,
}
}
fn find_or_create_ip_peer(
hosts: &mut Vec<DiscoveredHost>,
ip_to_host: &mut HashMap<IpAddr, usize>,
exclude_idx: Option<usize>,
is_v6: bool,
observed_mac: [u8; 6],
is_l3_only: bool,
) -> usize {
if is_unicast_mac(observed_mac)
&& let Some((idx, _)) = hosts.iter().enumerate().find(|(i, h)| {
Some(*i) != exclude_idx
&& h.ip.is_some_and(|ip| ip.is_ipv6() == is_v6)
&& h.mac == Some(observed_mac)
})
{
return idx;
}
if let Some((idx, _)) = hosts
.iter()
.enumerate()
.find(|(i, h)| Some(*i) != exclude_idx && h.ip.is_some_and(|ip| ip.is_ipv6() == is_v6))
{
return idx;
}
let mut candidate_idx = 254u16;
let new_ip = loop {
let ip = if is_v6 {
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, candidate_idx))
} else {
IpAddr::V4(Ipv4Addr::new(192, 0, 2, (candidate_idx & 0xff) as u8))
};
if !ip_to_host.contains_key(&ip) {
break ip;
}
candidate_idx = candidate_idx.wrapping_sub(1).max(1);
};
let new_idx = hosts.len();
let mac = if is_l3_only {
None
} else if is_unicast_mac(observed_mac) {
Some(observed_mac)
} else if exclude_idx.is_none() {
Some([0x00, 0x00, 0x00, 0x00, 0x00, 0x01])
} else {
Some([0x00, 0x00, 0x00, 0x00, 0x00, 0x02])
};
hosts.push(DiscoveredHost {
name: format!("host_{}", new_idx + 1),
ip: Some(new_ip),
mac,
});
ip_to_host.insert(new_ip, new_idx);
new_idx
}
fn find_or_create_l2_peer(
hosts: &mut Vec<DiscoveredHost>,
mac_to_l2_host: &mut HashMap<[u8; 6], usize>,
exclude_idx: Option<usize>,
) -> usize {
if let Some((idx, _)) = hosts
.iter()
.enumerate()
.find(|(i, h)| Some(*i) != exclude_idx && h.ip.is_none())
{
return idx;
}
let mut suffix = if exclude_idx.is_none() { 1u8 } else { 2u8 };
let mac = loop {
let candidate = [0x00, 0x00, 0x00, 0x00, 0x00, suffix];
if !mac_to_l2_host.contains_key(&candidate)
&& !hosts.iter().any(|h| h.mac == Some(candidate))
{
break candidate;
}
suffix = suffix.wrapping_add(1).max(1);
};
let new_idx = hosts.len();
hosts.push(DiscoveredHost {
name: format!("host_{}", new_idx + 1),
ip: None,
mac: Some(mac),
});
mac_to_l2_host.insert(mac, new_idx);
new_idx
}