docs: add LLDP topology discovery analysis from lldp2map research

- Comprehensive analysis of lldp2map's discovery algorithms
- BFS recursive topology discovery with depth limits
- LLDP-MIB walking with smart fallback strategies
- Recommended Elixir/Phoenix implementation phases
- Database schema for device neighbors
- Topology visualization options
- Auto-discovery workflow enhancements
- Estimated 8-11 days implementation effort

Based on research of https://github.com/buraglio/lldp2map
This commit is contained in:
Graham McIntire 2026-03-05 10:39:09 -06:00
parent b216da96f5
commit 4c7c4d7714
No known key found for this signature in database

View file

@ -0,0 +1,400 @@
# LLDP Topology Discovery Analysis for Towerops
## Executive Summary
`lldp2map` is a well-architected Go CLI tool that performs **recursive network topology discovery** using LLDP (Link Layer Discovery Protocol) via SNMP. This analysis examines how its approach could significantly enhance Towerops' device discovery and network visualization capabilities.
## Current Towerops Discovery Limitations
1. **Manual Device Addition**: Users must manually add each device's IP address
2. **No Topology Awareness**: No understanding of physical network connections
3. **No Automatic Neighbor Discovery**: Missing devices that are reachable via LLDP neighbors
4. **No Visual Network Maps**: No built-in topology visualization
5. **Limited Relationship Data**: Device relationships not captured or displayed
## lldp2map Key Features & Algorithms
### 1. Recursive BFS (Breadth-First Search) Discovery
**Algorithm** (from `cmd/root.go:138-215`):
```go
queue := []queueItem{{host: seedHost, depth: 0}}
visited := map[string]bool{}
for len(queue) > 0 {
item := queue[0]
queue = queue[1:]
if visited[item.host] {
continue
}
visited[item.host] = true
// Walk LLDP neighbors
info, err := lldp.Walk(client)
// Add discovered neighbors to queue
for _, neighbor := range info.Neighbors {
for _, ip := range neighbor.MgmtAddrs {
if !visited[ipStr] && item.depth < maxHops {
queue = append(queue, queueItem{
host: ipStr,
depth: item.depth + 1
})
}
}
}
}
```
**Benefits**:
- Automatically discovers entire network from a single seed device
- Configurable depth limit prevents runaway discovery
- Handles network cycles via visited tracking
- Scales to large networks efficiently
### 2. Comprehensive LLDP MIB Walking
**OIDs Used** (from `internal/lldp/walker.go:13-45`):
```
Local System:
- 1.0.8802.1.1.2.1.3.3.0 lldpLocSysName (device hostname)
- 1.0.8802.1.1.2.1.3.7.1.4 lldpLocPortDesc (local port names)
Remote Neighbors:
- 1.0.8802.1.1.2.1.4.1.1.7 lldpRemPortId (remote port ID)
- 1.0.8802.1.1.2.1.4.1.1.8 lldpRemPortDesc (remote port name)
- 1.0.8802.1.1.2.1.4.1.1.9 lldpRemSysName (neighbor hostname)
- 1.0.8802.1.1.2.1.4.2.1.3 lldpRemManAddrIfId (management IPs)
- 1.0.8802.1.1.2.1.4.1.1.4 lldpRemChassisIdSubtype
- 1.0.8802.1.1.2.1.4.1.1.5 lldpRemChassisId
Interface Addresses:
- 1.3.6.1.2.1.4.34.1.3 ipAddressIfIndex (IPv4+IPv6, RFC 4293)
- 1.3.6.1.2.1.4.20.1.1 ipAdEntAddr (IPv4 fallback, RFC 1213)
ARP Resolution:
- 1.3.6.1.2.1.4.22.1.2 ipNetToPhysAddr (MAC→IP resolution)
```
**Smart Fallback Strategy**:
1. Try modern management address table (`lldpRemManAddrIfId`)
2. Fall back to chassis ID with network address type
3. For MAC chassis IDs, resolve via device ARP table
4. This ensures maximum compatibility across device vendors
### 3. Advanced OID Index Parsing
**Example: Management Address Index** (`internal/lldp/walker.go:242-284`):
```
Index format: timeMark.portNum.remIndex.addrSubtype.addrLen.addr[bytes]
- addrSubtype: 1=IPv4 (4 bytes), 2=IPv6 (16 bytes)
- Handles both IPv4 and IPv6 natively
- Extracts neighbor identification across multiple MIB entries
```
**Parsing robustness**:
- Handles malformed OID responses gracefully
- Filters out loopback and link-local addresses
- Deduplicates addresses across multiple discovery paths
### 4. Topology Graph Structure
**Data Model** (`internal/graph/topology.go`):
```go
type Node struct {
Name string // Device hostname
IP string // Management IP for SNMP
Addrs []string // All interface IPs (optional)
}
type Edge struct {
From string // Source device name
To string // Destination device name
FromPort string // Local port (eth0, ge-0/0/1, etc)
ToPort string // Remote port
}
```
**Edge Deduplication**:
- Automatically removes A→B when B→A exists
- Prevents duplicate links in visual diagrams
- Maintains bidirectional link information
## Recommended Implementation for Towerops
### Phase 1: Core LLDP Discovery (Elixir Migration)
**New Elixir Modules**:
```elixir
# lib/towerops/snmp/lldp.ex
defmodule Towerops.Snmp.Lldp do
@moduledoc """
LLDP topology discovery via SNMP.
Walks LLDP-MIB tables to discover network neighbors.
"""
@oid_loc_sys_name "1.0.8802.1.1.2.1.3.3.0"
@oid_rem_sys_name "1.0.8802.1.1.2.1.4.1.1.9"
@oid_rem_man_addr "1.0.8802.1.1.2.1.4.2.1.3"
def discover_neighbors(device_id) do
# Walk LLDP tables
# Parse neighbor data
# Return list of %{hostname, ports, management_ips}
end
end
# lib/towerops/topology/discovery.ex
defmodule Towerops.Topology.Discovery do
@moduledoc """
Recursive network topology discovery using LLDP.
"""
def discover_from_seed(device_id, opts \\\\ []) do
max_depth = Keyword.get(opts, :max_depth, 10)
# BFS queue implementation
# Track visited devices
# Build topology graph
end
end
# lib/towerops/topology/neighbor.ex (new schema)
defmodule Towerops.Topology.Neighbor do
@moduledoc """
Represents an LLDP neighbor relationship between devices.
"""
schema "device_neighbors" do
belongs_to :device, Device
belongs_to :neighbor_device, Device
field :local_port, :string
field :remote_port, :string
field :discovered_at, :utc_datetime
field :last_seen_at, :utc_datetime
timestamps()
end
end
```
**Database Schema**:
```sql
CREATE TABLE device_neighbors (
id UUID PRIMARY KEY,
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
neighbor_device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
local_port TEXT NOT NULL,
remote_port TEXT,
discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
inserted_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(device_id, local_port, neighbor_device_id)
);
CREATE INDEX idx_neighbors_device ON device_neighbors(device_id);
CREATE INDEX idx_neighbors_neighbor ON device_neighbors(neighbor_device_id);
```
### Phase 2: Topology Worker (Background Discovery)
**New Oban Worker**:
```elixir
defmodule Towerops.Workers.TopologyDiscoveryWorker do
use Oban.Worker, queue: :discovery, max_attempts: 3
@impl Oban.Worker
def perform(%Oban.Job{args: %{"seed_device_id" => device_id}}) do
# Run discovery from seed device
# Update device_neighbors table
# Create new devices for unknown neighbors
# Broadcast topology update to LiveView
:ok
end
end
```
**Trigger Options**:
1. **Manual**: User clicks "Discover Topology" button
2. **Scheduled**: Nightly cron job for each site
3. **Event-Driven**: When new device added with LLDP enabled
### Phase 3: Topology Visualization (LiveView Component)
**New LiveView**:
```elixir
defmodule ToweropsWeb.TopologyLive.Show do
use ToweropsWeb, :live_view
def mount(%{"site_id" => site_id}, _session, socket) do
neighbors = Topology.list_site_neighbors(site_id)
{:ok,
socket
|> assign(:site_id, site_id)
|> assign(:neighbors, neighbors)
|> assign(:layout_format, "force_directed")}
end
def handle_event("run_discovery", %{"device_id" => device_id}, socket) do
TopologyDiscoveryWorker.enqueue(%{seed_device_id: device_id})
{:noreply, put_flash(socket, :info, "Topology discovery started")}
end
end
```
**Visualization Options**:
1. **SVG Force-Directed Graph**: Use D3.js or Cytoscape.js via Alpine.js hook
2. **Interactive Canvas**: vis-network or sigma.js
3. **Export to Draw.io**: Generate XML for external editing
4. **Export to PNG/PDF**: Server-side Graphviz rendering
### Phase 4: Device Auto-Discovery Enhancement
**Current Flow**:
```
User manually adds device → SNMP discovery runs → Sensors created
```
**Enhanced Flow with LLDP**:
```
User adds seed device →
SNMP discovery runs →
LLDP neighbor discovery runs →
New devices auto-created (pending approval) →
Recursive SNMP discovery on approved neighbors
```
**UI Changes**:
```elixir
# New "Pending Devices" tab on Devices page
def handle_event("approve_discovered_device", %{"id" => id}, socket) do
case Devices.approve_discovered_device(id) do
{:ok, device} ->
# Enable SNMP polling
# Run discovery on new device
# Continue topology discovery if depth allows
{:error, _} ->
# Show error
end
end
```
## Benefits for Towerops
### For Network Operators
1. **Faster Onboarding**: Add one device, discover entire network
2. **Topology Awareness**: Visual understanding of network structure
3. **Change Detection**: Alert when topology changes (new/missing links)
4. **Documentation**: Auto-generated network diagrams for customers
5. **Troubleshooting**: See physical path between devices
### For Developers
1. **Path Analysis**: Calculate shortest path between devices
2. **Impact Analysis**: "If this device fails, what's affected?"
3. **Capacity Planning**: Identify bottleneck links
4. **Configuration Validation**: Verify redundancy in topology
### Technical Advantages
1. **Standards-Based**: LLDP is IEEE 802.1AB standard, vendor-neutral
2. **Low Overhead**: Single SNMP walk per device
3. **Incremental Updates**: Only re-scan changed devices
4. **Scalable**: BFS with depth limit prevents resource exhaustion
## Implementation Complexity
**Estimated Effort**:
- Phase 1 (Core Discovery): 2-3 days
- Phase 2 (Background Worker): 1 day
- Phase 3 (Visualization): 3-5 days
- Phase 4 (Auto-Discovery): 2 days
- **Total**: 8-11 days
**Dependencies**:
- No new external libraries needed (use existing SnmpKit)
- Graphviz optional (only for export features)
- D3.js or Cytoscape.js for visualization (already using Alpine.js)
## Risks & Mitigation
**Risks**:
1. **LLDP Not Enabled**: Some devices don't have LLDP enabled
- *Mitigation*: Provide instructions to enable, support manual links
2. **Management Address Missing**: Neighbor has no mgmt IP
- *Mitigation*: Use chassis ID fallback like lldp2map does
3. **Large Network Scan**: Could overwhelm SNMP on many devices
- *Mitigation*: Rate limiting, depth limits, async background jobs
4. **Stale Data**: Topology changes not reflected in real-time
- *Mitigation*: Periodic re-scan, "last_seen_at" timestamps
## Comparison: lldp2map vs Towerops Integration
| Feature | lldp2map (Go CLI) | Towerops Enhancement |
|---------|-------------------|----------------------|
| Discovery | One-time, manual | Background cron + manual trigger |
| Storage | In-memory graph | PostgreSQL with TimescaleDB |
| Visualization | Static PNG/PDF | Interactive LiveView with real-time updates |
| Integration | Standalone | Integrated with existing devices, sites, alerts |
| Multi-tenancy | None | Organization/site scoped |
| Authentication | CLI flags | Inherited from device SNMP credentials |
| History | None | Track topology changes over time |
| Alerting | None | Alert on topology changes |
## Recommended Next Steps
1. **Prototype** (1 day):
- Create basic `Lldp.discover_neighbors/1` function
- Test on production devices to verify LLDP support
- Validate OID parsing logic
2. **Schema & Migration** (0.5 days):
- Add `device_neighbors` table
- Create Ecto schema and context functions
3. **Worker Implementation** (1 day):
- Build `TopologyDiscoveryWorker`
- Add manual trigger UI
4. **Visualization** (3-5 days):
- Choose visualization library (recommend Cytoscape.js)
- Build interactive topology view
- Add export options
5. **Auto-Discovery** (2 days):
- Pending devices workflow
- Approval UI and logic
## Code Quality Notes
**lldp2map Strengths to Adopt**:
1. ✅ **Comprehensive OID coverage** - handles multiple MIB table formats
2. ✅ **Smart fallbacks** - gracefully degrades when data missing
3. ✅ **Clean separation** - SNMP, LLDP, Graph, Render all separate
4. ✅ **Robust parsing** - handles malformed/unexpected SNMP responses
5. ✅ **IPv6 support** - first-class support, not an afterthought
**Additional Improvements for Towerops**:
1. **Historical tracking** - store topology snapshots for trend analysis
2. **Change alerting** - notify when new neighbors appear/disappear
3. **Multi-site correlation** - show connections between sites
4. **Performance metrics** - link utilization on topology edges
5. **Agent integration** - prefer local agent polling for LLDP data
## Conclusion
lldp2map demonstrates a **production-ready approach to network topology discovery** that would significantly enhance Towerops' value proposition. The recursive BFS algorithm, comprehensive LLDP MIB coverage, and smart fallback mechanisms provide a solid foundation.
By adapting this approach to Elixir/Phoenix with background workers, persistent storage, and interactive visualization, Towerops can offer **automated network discovery and topology mapping** as a key differentiator from manual monitoring tools.
**Recommendation**: Implement Phases 1-2 (8-11 days) to provide immediate value, then iterate on visualization and auto-discovery based on customer feedback.