diff --git a/arp.go b/arp.go new file mode 100644 index 0000000..f1a5d0c --- /dev/null +++ b/arp.go @@ -0,0 +1,114 @@ +package tendrils + +import ( + "bufio" + "context" + "log" + "net" + "os/exec" + "runtime" + "strings" + "time" +) + +func (t *Tendrils) pollARP(ctx context.Context) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + t.readARPTable() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + t.readARPTable() + } + } +} + +type arpEntry struct { + ip net.IP + mac net.HardwareAddr + iface string +} + +func (t *Tendrils) readARPTable() { + entries := t.parseARPTable() + + for _, entry := range entries { + if isBroadcastOrZero(entry.mac) { + continue + } + + t.nodes.Update([]net.IP{entry.ip}, []net.HardwareAddr{entry.mac}, entry.iface, "", "arp") + } +} + +func (t *Tendrils) parseARPTable() []arpEntry { + if runtime.GOOS == "darwin" { + return t.parseARPDarwin() + } + return t.parseARPLinux() +} + +func (t *Tendrils) parseARPDarwin() []arpEntry { + cmd := exec.Command("arp", "-a") + output, err := cmd.Output() + if err != nil { + return nil + } + + var entries []arpEntry + scanner := bufio.NewScanner(strings.NewReader(string(output))) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < 6 { + continue + } + + ipStr := strings.Trim(fields[1], "()") + ip := net.ParseIP(ipStr) + if ip == nil { + continue + } + + macStr := fields[3] + if macStr == "(incomplete)" { + continue + } + + macStr = normalizeMACAddress(macStr) + mac, err := net.ParseMAC(macStr) + if err != nil { + log.Printf("[arp] failed to parse MAC %q for IP %s: %v", macStr, ipStr, err) + continue + } + + ifaceName := fields[5] + + entries = append(entries, arpEntry{ + ip: ip, + mac: mac, + iface: ifaceName, + }) + } + + return entries +} + +func (t *Tendrils) parseARPLinux() []arpEntry { + var entries []arpEntry + return entries +} + +func normalizeMACAddress(mac string) string { + parts := strings.Split(mac, ":") + for i, part := range parts { + if len(part) == 1 { + parts[i] = "0" + part + } + } + return strings.Join(parts, ":") +} diff --git a/go.mod b/go.mod index 2198ae6..ca074f2 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/gopatchy/tendrils go 1.24.4 -require github.com/google/gopacket v1.1.19 +require ( + github.com/google/gopacket v1.1.19 + github.com/mostlygeek/arp v0.0.0-20170424181311-541a2129847a +) require golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect diff --git a/go.sum b/go.sum index a915606..ed192df 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/mostlygeek/arp v0.0.0-20170424181311-541a2129847a h1:AfneHvfmYgUIcgdUrrDFklLdEzQAvG9AKRTe1x1mx/0= +github.com/mostlygeek/arp v0.0.0-20170424181311-541a2129847a/go.mod h1:jZxafo9CAqaKFQE4zitrg5QNlA6CXUsjwXPlIppF3tk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= diff --git a/lldp.go b/lldp.go index 989b52f..bc2dc2a 100644 --- a/lldp.go +++ b/lldp.go @@ -77,5 +77,17 @@ func isBroadcastOrZero(mac net.HardwareAddr) bool { } } - return allZero || allFF + if allZero || allFF { + return true + } + + if mac[0] == 0x01 && mac[1] == 0x00 && mac[2] == 0x5e { + return true + } + + if mac[0] == 0x33 && mac[1] == 0x33 { + return true + } + + return false } diff --git a/nodes.go b/nodes.go index 7e8a48a..689bd5b 100644 --- a/nodes.go +++ b/nodes.go @@ -197,31 +197,96 @@ func (n *Nodes) logNode(id int, prefix string, isLast bool) { if id == 0 { log.Printf("[root] %s", node) + n.logChildrenByInterface(id, "") } else { connector := "├──" if isLast { connector = "└──" } - if node.ParentPort != "" && node.LocalPort != "" { - log.Printf("%s%s %s -> %s on %s", prefix, connector, node.ParentPort, node.LocalPort, node) - } else { - log.Printf("%s%s %s", prefix, connector, node) + childPort := node.LocalPort + if childPort == "" { + childPort = "??" } - } - children := n.getChildren(id) - for i, childID := range children { - childIsLast := i == len(children)-1 - childPrefix := prefix - if id != 0 { + log.Printf("%s%s %s on %s", prefix, connector, childPort, node) + + children := n.getChildren(id) + for i, childID := range children { + childIsLast := i == len(children)-1 + childPrefix := prefix if isLast { childPrefix += " " } else { childPrefix += "│ " } + n.logNode(childID, childPrefix, childIsLast) + } + } +} + +func (n *Nodes) logChildrenByInterface(parentID int, prefix string) { + children := n.getChildren(parentID) + + byInterface := map[string][]int{} + for _, childID := range children { + child := n.nodes[childID] + iface := child.ParentPort + if iface == "" { + iface = "??" + } + byInterface[iface] = append(byInterface[iface], childID) + } + + var interfaces []string + for iface := range byInterface { + interfaces = append(interfaces, iface) + } + sort.Strings(interfaces) + + for i, iface := range interfaces { + isLastInterface := i == len(interfaces)-1 + connector := "├──" + if isLastInterface { + connector = "└──" + } + + log.Printf("%s%s %s", prefix, connector, iface) + + nodes := byInterface[iface] + for j, nodeID := range nodes { + isLastNode := j == len(nodes)-1 + nodeConnector := "├──" + if isLastNode { + nodeConnector = "└──" + } + + nodePrefix := prefix + if isLastInterface { + nodePrefix += " " + } else { + nodePrefix += "│ " + } + + node := n.nodes[nodeID] + childPort := node.LocalPort + if childPort == "" { + childPort = "??" + } + + log.Printf("%s%s %s on %s", nodePrefix, nodeConnector, childPort, node) + + grandchildren := n.getChildren(nodeID) + if len(grandchildren) > 0 { + grandchildPrefix := nodePrefix + if isLastNode { + grandchildPrefix += " " + } else { + grandchildPrefix += "│ " + } + n.logChildrenByInterface(nodeID, grandchildPrefix) + } } - n.logNode(childID, childPrefix, childIsLast) } } diff --git a/tendrils.go b/tendrils.go index 2f1cc8e..cc198ce 100644 --- a/tendrils.go +++ b/tendrils.go @@ -20,6 +20,11 @@ func New() *Tendrils { } func (t *Tendrils) Run() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go t.pollARP(ctx) + ticker := time.NewTicker(1 * time.Second) defer ticker.Stop()