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:
parent
b216da96f5
commit
4c7c4d7714
1 changed files with 400 additions and 0 deletions
400
docs/lldp-topology-discovery-analysis.md
Normal file
400
docs/lldp-topology-discovery-analysis.md
Normal 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.
|
||||
Loading…
Add table
Reference in a new issue