net-mumu 0.2.0-rc.3

Network tools plugin for the Lava language
Documentation
// src/lldp/parse.rs
#![allow(dead_code)]

//
// LLDP/CDP TLV parsing → LldpRow
// -------------------------------
// This module converts raw Ethernet frames (already captured by `capture.rs`)
// into the higher-level `LldpRow` records used by the rest of the plugin.
//
// • No stubs/fallbacks: if a frame cannot be parsed, we return `None` so the
//   caller can see that discovery did not work for that frame.
// • LLDP: we parse the basic TLVs (chassis-id, port-id, TTL, system/port desc,
//   system name) and also look for System Capabilities, Management Address,
//   and the IEEE 802.1 organizational VLAN (PVID) TLV.
// • CDP: we parse the header (version, TTL) and the common TLVs (Device-ID,
//   Port-ID, Capabilities, Native VLAN, Management Address) and build a row.
//   The chassis_id is derived from the Ethernet source MAC.
//
// This file contains *no* MuMu runtime logic; it’s a pure parser/adapter.

use std::net::{Ipv4Addr, Ipv6Addr};

use super::proto::DiscoveryProtocol;
use super::row::LldpRow;
use super::capture::{
    parse_lldp_basic,
    fmt_identifier,
    lldp_payload,
    cdp_payload,
};

/* ───────────────────────────── Public entry points ───────────────────────────── */

/// Parse an Ethernet frame into an `LldpRow` if it carries LLDP or CDP.
pub fn parse_any_row_from_frame(iface: &str, frame: &[u8], timestamp_ms: u64) -> Option<LldpRow> {
    if let Some(row) = parse_lldp_row_from_frame(iface, frame, timestamp_ms) {
        return Some(row);
    }
    if let Some(row) = parse_cdp_row_from_frame(iface, frame, timestamp_ms) {
        return Some(row);
    }
    None
}

/// Parse an LLDP frame into `LldpRow`. Returns `None` if the frame is not LLDP or is malformed.
pub fn parse_lldp_row_from_frame(iface: &str, frame: &[u8], timestamp_ms: u64) -> Option<LldpRow> {
    let payload = lldp_payload(frame)?;
    let basic = parse_lldp_basic(payload)?;

    // Chassis/port formatting
    let chassis_s = basic
        .chassis_id
        .as_ref()
        .map(|b| fmt_identifier(basic.chassis_id_subtype, b))
        .unwrap_or_else(|| "<unknown>".to_string());

    let port_s = basic
        .port_id
        .as_ref()
        .map(|b| fmt_identifier(basic.port_id_subtype, b))
        .unwrap_or_else(|| "<unknown>".to_string());

    // Extras: capabilities / management IP / VLAN (PVID)
    let (caps, mgmt_ip, vlan) = parse_lldp_extras(payload);

    Some(LldpRow {
        interface: iface.to_string(),
        protocol: DiscoveryProtocol::LLDP,
        chassis_id: chassis_s,
        port_id: port_s,
        system_name: basic.system_name.clone(),
        system_desc: basic.system_desc.clone(),
        port_desc: basic.port_desc.clone(),
        vlan,
        management_ip: mgmt_ip,
        capabilities: caps,
        ttl: basic.ttl,
        timestamp_ms,
    })
}

/// Parse a CDP frame into `LldpRow`. Returns `None` if the frame is not CDP or is malformed.
///
/// CDP format (simplified):
///   Header: version(1), ttl(1), checksum(2)
///   TLVs:   type(2), length(2), value(length-4)
///
/// We look for Device-ID, Port-ID, Capabilities, Native VLAN, Management Address.
pub fn parse_cdp_row_from_frame(iface: &str, frame: &[u8], timestamp_ms: u64) -> Option<LldpRow> {
    let payload = cdp_payload(frame)?;

    if payload.len() < 4 {
        return None;
    }
    let version = payload[0];
    let ttl_u8 = payload[1];
    let _checksum = u16::from_be_bytes([payload[2], payload[3]]);

    // Extract source MAC as chassis_id
    if frame.len() < 12 {
        return None;
    }
    let src_mac = &frame[6..12];
    let chassis_s = format!(
        "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
        src_mac[0], src_mac[1], src_mac[2], src_mac[3], src_mac[4], src_mac[5]
    );

    let mut system_name: Option<String> = None; // Device-ID
    let mut port_id: Option<String> = None;     // Port-ID
    let mut system_desc: Option<String> = None; // From Platform/Version (if present)
    let mut vlan: Option<String> = None;        // Native VLAN
    let mut mgmt_ip: Option<String> = None;     // First IPv4/IPv6 in Addresses
    let mut capabilities: Vec<String> = Vec::new();

    let mut i = 4usize;
    while i + 4 <= payload.len() {
        let tlv_type = u16::from_be_bytes([payload[i], payload[i + 1]]);
        let tlv_len  = u16::from_be_bytes([payload[i + 2], payload[i + 3]]) as usize;
        if tlv_len < 4 || i + tlv_len > payload.len() {
            // malformed TLV; stop parsing
            break;
        }
        let value = &payload[i + 4 .. i + tlv_len];

        match tlv_type {
            0x0001 => { // Device-ID
                if let Ok(s) = String::from_utf8(value.to_vec()) {
                    if !s.is_empty() { system_name = Some(s); }
                }
            }
            0x0003 => { // Port-ID
                if let Ok(s) = String::from_utf8(value.to_vec()) {
                    if !s.is_empty() { port_id = Some(s); }
                }
            }
            0x0004 => { // Capabilities (4 bytes)
                if value.len() >= 4 {
                    let caps = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
                    capabilities = decode_cdp_capabilities(caps);
                }
            }
            0x000a => { // Native VLAN (2 bytes)
                if value.len() >= 2 {
                    let vid = u16::from_be_bytes([value[0], value[1]]);
                    vlan = Some(vid.to_string());
                }
            }
            0x0002 => { // Addresses (complex structure)
                if let Some(addr_s) = parse_cdp_addresses(value) {
                    mgmt_ip = Some(addr_s);
                }
            }
            0x0005 => { // Software version
                if let Ok(s) = String::from_utf8(value.to_vec()) {
                    system_desc = Some(match system_desc.take() {
                        Some(prev) => format!("{} | {}", prev, s),
                        None => s,
                    });
                }
            }
            0x0006 => { // Platform
                if let Ok(s) = String::from_utf8(value.to_vec()) {
                    system_desc = Some(match system_desc.take() {
                        Some(prev) => format!("{} | {}", prev, s),
                        None => s,
                    });
                }
            }
            _ => { /* ignore other TLVs */ }
        }

        i += tlv_len;
    }

    // If missing Port-ID, try a sane default
    let port_id_f = port_id.unwrap_or_else(|| "<unknown>".into());

    Some(LldpRow {
        interface: iface.to_string(),
        protocol: DiscoveryProtocol::CDP,
        chassis_id: chassis_s,
        port_id: port_id_f,
        system_name,
        system_desc,
        port_desc: None,
        vlan,
        management_ip: mgmt_ip,
        capabilities,
        ttl: Some(ttl_u8 as u16),
        timestamp_ms,
    }).filter(|_| version != 0) // very basic sanity guard
}

/* ───────────────────────────── LLDP helpers ───────────────────────────── */

fn parse_lldp_extras(payload: &[u8]) -> (Vec<String>, Option<String>, Option<String>) {
    let mut caps_out: Vec<String> = Vec::new();
    let mut mgmt_ip: Option<String> = None;
    let mut vlan: Option<String> = None;

    let mut i = 0usize;
    while i < payload.len() {
        let (typ, len) = match tlv_header(payload, &mut i) {
            Some(x) => x,
            None => break,
        };
        if len == 0 && typ == 0 { break; }
        if i + len > payload.len() { break; }

        match typ {
            7 => {
                // System Capabilities: 2 bytes "system", 2 bytes "enabled"
                if len >= 4 {
                    let enabled = u16::from_be_bytes([payload[i + 2], payload[i + 3]]);
                    caps_out = decode_lldp_capabilities(enabled);
                }
            }
            8 => {
                // Management Address
                if mgmt_ip.is_none() {
                    if let Some(addr) = parse_lldp_mgmt_addr(&payload[i .. i + len]) {
                        mgmt_ip = Some(addr);
                    }
                }
            }
            127 => {
                // Organizationally Specific — look for IEEE 802.1 (00-80-C2) Port VLAN ID (subtype 1)
                if len >= 4 && payload[i..i + 3] == [0x00, 0x80, 0xC2] {
                    if payload[i + 3] == 0x01 && len >= 6 {
                        let pvid = u16::from_be_bytes([payload[i + 4], payload[i + 5]]);
                        vlan = Some(pvid.to_string());
                    }
                }
            }
            _ => {}
        }

        i += len;
    }

    (caps_out, mgmt_ip, vlan)
}

fn decode_lldp_capabilities(bits: u16) -> Vec<String> {
    let mut out = Vec::new();
    if bits & 0x0002 != 0 { out.push("repeater".into()); }
    if bits & 0x0004 != 0 { out.push("bridge".into()); }
    if bits & 0x0008 != 0 { out.push("wlan-ap".into()); }
    if bits & 0x0010 != 0 { out.push("router".into()); }
    if bits & 0x0020 != 0 { out.push("telephone".into()); }
    if bits & 0x0040 != 0 { out.push("docsis".into()); }
    if bits & 0x0080 != 0 { out.push("station".into()); }
    if bits & 0x0100 != 0 { out.push("cvlan".into()); }
    if bits & 0x0200 != 0 { out.push("svlan".into()); }
    if bits & 0x0800 != 0 { out.push("tpmr".into()); }
    out
}

fn parse_lldp_mgmt_addr(tlv_value: &[u8]) -> Option<String> {
    // TLV 8 value layout:
    //  0: mgmt addr string length (L) — includes subtype (1) + address bytes (A)
    //  1: mgmt addr subtype (1=IPv4, 2=IPv6, 6=MAC, ...)
    //  2..(1+A): address bytes
    //  next: interface numbering subtype (1)
    //  next: interface number (4)
    //  next: OID length (1)
    //  next: OID bytes (var)
    if tlv_value.len() < 1 { return None; }
    let l = tlv_value[0] as usize;
    if tlv_value.len() < 1 + l + 1 + 4 + 1 {
        return None;
    }
    let subtype = tlv_value[1];
    let addr_bytes = &tlv_value[2 .. 1 + l]; // (l includes subtype byte)
    match subtype {
        1 => { // IPv4
            if addr_bytes.len() == 4 {
                let ip = Ipv4Addr::new(addr_bytes[0], addr_bytes[1], addr_bytes[2], addr_bytes[3]);
                return Some(ip.to_string());
            }
        }
        2 => { // IPv6
            if addr_bytes.len() == 16 {
                let mut segs = [0u16; 8];
                for i in 0..8 {
                    segs[i] = u16::from_be_bytes([addr_bytes[i*2], addr_bytes[i*2+1]]);
                }
                let ip = Ipv6Addr::new(
                    segs[0], segs[1], segs[2], segs[3],
                    segs[4], segs[5], segs[6], segs[7]
                );
                return Some(ip.to_string());
            }
        }
        _ => {}
    }
    None
}

/// Reparse a TLV header at index (duplicated to keep this module self-contained)
#[inline]
fn tlv_header(buf: &[u8], idx: &mut usize) -> Option<(u8, usize)> {
    if buf.len() < *idx + 2 { return None; }
    let b1 = buf[*idx];
    let b2 = buf[*idx + 1];
    *idx += 2;
    let typ = (b1 >> 1) & 0x7F;
    let len = (((b1 as u16 & 0x01) << 8) | b2 as u16) as usize;
    Some((typ, len))
}

/* ───────────────────────────── CDP helpers ───────────────────────────── */

fn decode_cdp_capabilities(bits: u32) -> Vec<String> {
    // Map a *common* subset of CDP capability flags to strings.
    // This is intentionally conservative to avoid mis-labeling.
    let mut out = Vec::new();
    if bits & 0x00000001 != 0 { out.push("router".into()); }
    if bits & 0x00000002 != 0 { out.push("bridge".into()); }      // transparent bridge
    if bits & 0x00000004 != 0 { out.push("srb".into()); }         // source-route bridge
    if bits & 0x00000008 != 0 { out.push("switch".into()); }      // L2 switch
    if bits & 0x00000010 != 0 { out.push("l3-switch".into()); }   // L3 switch
    if bits & 0x00000020 != 0 { out.push("host".into()); }
    if bits & 0x00000080 != 0 { out.push("repeater".into()); }
    out
}

fn parse_cdp_addresses(mut v: &[u8]) -> Option<String> {
    if v.len() < 4 { return None; }
    let num = u32::from_be_bytes([v[0], v[1], v[2], v[3]]) as usize;
    v = &v[4..];
    let mut first_ip: Option<String> = None;

    for _ in 0..num {
        if v.len() < 2 { break; }
        let proto_type = v[0]; // 1 => NLPID, 2 => 802.2
        let proto_len  = v[1] as usize;
        v = &v[2..];
        if v.len() < proto_len + 2 { break; }
        let proto = &v[..proto_len];
        v = &v[proto_len..];

        // Address length + address
        let addr_len = u16::from_be_bytes([v[0], v[1]]) as usize;
        v = &v[2..];
        if v.len() < addr_len { break; }
        let addr = &v[..addr_len];
        v = &v[addr_len..];

        // Heuristics:
        //   NLPID 0xCC often denotes IP; addr_len 4 => IPv4
        if proto_type == 1 /* NLPID */ {
            if proto_len == 1 && proto[0] == 0xCC && addr_len == 4 {
                let ip = Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]).to_string();
                return Some(ip);
            }
        }

        // Fallbacks: try to guess IPv4/IPv6 by length
        if addr_len == 4 {
            let ip = Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]).to_string();
            if first_ip.is_none() { first_ip = Some(ip); }
        } else if addr_len == 16 {
            let mut segs = [0u16; 8];
            for i in 0..8 {
                segs[i] = u16::from_be_bytes([addr[i*2], addr[i*2+1]]);
            }
            let ip6 = Ipv6Addr::new(
                segs[0], segs[1], segs[2], segs[3],
                segs[4], segs[5], segs[6], segs[7]
            ).to_string();
            if first_ip.is_none() { first_ip = Some(ip6); }
        }
    }

    first_ip
}