diff --git a/CLAUDE.md b/CLAUDE.md index 00298e2..248ed5c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,4 +2,6 @@ - Use e.g. map[string]bool{} instead of make() wherever possible - Use all-lowercase log messages - Prepend log messages with [ERROR] if applicable -- Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence \ No newline at end of file +- Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence +- Always push after commiting +- Use git add -A so you don't miss files when committing \ No newline at end of file diff --git a/lldp.go b/lldp.go new file mode 100644 index 0000000..f5dfa46 --- /dev/null +++ b/lldp.go @@ -0,0 +1,56 @@ +package tendrils + +import ( + "context" + "log" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" +) + +func (t *Tendrils) listenLLDP(ctx context.Context, iface net.Interface) { + handle, err := pcap.OpenLive(iface.Name, 65536, true, 5*time.Second) + if err != nil { + log.Printf("[ERROR] failed to open interface %s: %v", iface.Name, err) + return + } + defer handle.Close() + + if err := handle.SetBPFFilter("ether proto 0x88cc"); err != nil { + log.Printf("[ERROR] failed to set BPF filter on %s: %v", iface.Name, err) + return + } + + packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) + packets := packetSource.Packets() + + for { + select { + case <-ctx.Done(): + return + case packet, ok := <-packets: + if !ok { + return + } + t.handleLLDPPacket(iface.Name, packet) + } + } +} + +func (t *Tendrils) handleLLDPPacket(ifaceName string, packet gopacket.Packet) { + lldpLayer := packet.Layer(layers.LayerTypeLinkLayerDiscovery) + if lldpLayer == nil { + return + } + + lldp, ok := lldpLayer.(*layers.LinkLayerDiscovery) + if !ok { + return + } + + log.Printf("[%s] lldp packet received: ChassisID=%x PortID=%s TTL=%d", + ifaceName, lldp.ChassisID.ID, lldp.PortID.ID, lldp.TTL) +} diff --git a/tendrils.go b/tendrils.go index c2eb8fb..c00c9a9 100644 --- a/tendrils.go +++ b/tendrils.go @@ -2,23 +2,18 @@ package tendrils import ( "context" - "fmt" "log" "net" "time" - - "github.com/google/gopacket" - "github.com/google/gopacket/layers" - "github.com/google/gopacket/pcap" ) type Tendrils struct { - goroutines map[string]context.CancelFunc + activeInterfaces map[string]context.CancelFunc } func New() *Tendrils { return &Tendrils{ - goroutines: map[string]context.CancelFunc{}, + activeInterfaces: map[string]context.CancelFunc{}, } } @@ -28,7 +23,7 @@ func (t *Tendrils) Run() { for { interfaces := t.listInterfaces() - t.updateGoroutines(interfaces) + t.updateInterfaces(interfaces) <-ticker.C } } @@ -69,71 +64,30 @@ func (t *Tendrils) listInterfaces() []net.Interface { return validInterfaces } -func (t *Tendrils) updateGoroutines(interfaces []net.Interface) { +func (t *Tendrils) updateInterfaces(interfaces []net.Interface) { current := map[string]bool{} for _, iface := range interfaces { current[iface.Name] = true } - for name, cancel := range t.goroutines { + for name, cancel := range t.activeInterfaces { if !current[name] { log.Printf("interface removed: %s", name) cancel() - delete(t.goroutines, name) + delete(t.activeInterfaces, name) } } for _, iface := range interfaces { - if _, exists := t.goroutines[iface.Name]; !exists { + if _, exists := t.activeInterfaces[iface.Name]; !exists { log.Printf("interface added: %s", iface.Name) ctx, cancel := context.WithCancel(context.Background()) - t.goroutines[iface.Name] = cancel - go t.handleInterface(ctx, iface) + t.activeInterfaces[iface.Name] = cancel + t.startInterface(ctx, iface) } } } -func (t *Tendrils) handleInterface(ctx context.Context, iface net.Interface) { - handle, err := pcap.OpenLive(iface.Name, 65536, true, 5*time.Second) - if err != nil { - log.Printf("[ERROR] failed to open interface %s: %v", iface.Name, err) - return - } - defer handle.Close() - - bpfFilter := fmt.Sprintf("ether proto 0x88cc") - if err := handle.SetBPFFilter(bpfFilter); err != nil { - log.Printf("[ERROR] failed to set BPF filter on %s: %v", iface.Name, err) - return - } - - packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) - packets := packetSource.Packets() - - for { - select { - case <-ctx.Done(): - return - case packet, ok := <-packets: - if !ok { - return - } - t.handleLLDPPacket(iface.Name, packet) - } - } -} - -func (t *Tendrils) handleLLDPPacket(ifaceName string, packet gopacket.Packet) { - lldpLayer := packet.Layer(layers.LayerTypeLinkLayerDiscovery) - if lldpLayer == nil { - return - } - - lldp, ok := lldpLayer.(*layers.LinkLayerDiscovery) - if !ok { - return - } - - log.Printf("[%s] lldp packet received: ChassisID=%x PortID=%s TTL=%d", - ifaceName, lldp.ChassisID.ID, lldp.PortID.ID, lldp.TTL) +func (t *Tendrils) startInterface(ctx context.Context, iface net.Interface) { + go t.listenLLDP(ctx, iface) }