mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-04 01:07:49 +02:00
internal/clientinfo: add NDP discovery
This commit is contained in:
committed by
Cuong Manh Le
parent
4d996e317b
commit
cb445825f4
@@ -74,6 +74,7 @@ type Table struct {
|
|||||||
dhcp *dhcp
|
dhcp *dhcp
|
||||||
merlin *merlinDiscover
|
merlin *merlinDiscover
|
||||||
arp *arpDiscover
|
arp *arpDiscover
|
||||||
|
ndp *ndpDiscover
|
||||||
ptr *ptrDiscover
|
ptr *ptrDiscover
|
||||||
mdns *mdns
|
mdns *mdns
|
||||||
hf *hostsFile
|
hf *hostsFile
|
||||||
@@ -172,16 +173,28 @@ func (t *Table) init() {
|
|||||||
}
|
}
|
||||||
go t.dhcp.watchChanges()
|
go t.dhcp.watchChanges()
|
||||||
}
|
}
|
||||||
// ARP table.
|
// ARP/NDP table.
|
||||||
if t.discoverARP() {
|
if t.discoverARP() {
|
||||||
t.arp = &arpDiscover{}
|
t.arp = &arpDiscover{}
|
||||||
|
t.ndp = &ndpDiscover{}
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery")
|
||||||
if err := t.arp.refresh(); err != nil {
|
discovers := map[string]interface {
|
||||||
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init ARP discover")
|
refresher
|
||||||
} else {
|
IpResolver
|
||||||
t.ipResolvers = append(t.ipResolvers, t.arp)
|
MacResolver
|
||||||
t.macResolvers = append(t.macResolvers, t.arp)
|
}{
|
||||||
t.refreshers = append(t.refreshers, t.arp)
|
"ARP": t.arp,
|
||||||
|
"NDP": t.ndp,
|
||||||
|
}
|
||||||
|
|
||||||
|
for protocol, discover := range discovers {
|
||||||
|
if err := discover.refresh(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msgf("could not init %s discover", protocol)
|
||||||
|
} else {
|
||||||
|
t.ipResolvers = append(t.ipResolvers, discover)
|
||||||
|
t.macResolvers = append(t.macResolvers, discover)
|
||||||
|
t.refreshers = append(t.refreshers, discover)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// PTR lookup.
|
// PTR lookup.
|
||||||
@@ -328,7 +341,7 @@ func (t *Table) ListClients() []*Client {
|
|||||||
_ = r.refresh()
|
_ = r.refresh()
|
||||||
}
|
}
|
||||||
ipMap := make(map[string]*Client)
|
ipMap := make(map[string]*Client)
|
||||||
il := []ipLister{t.dhcp, t.arp, t.ptr, t.mdns, t.vni}
|
il := []ipLister{t.dhcp, t.arp, t.ndp, t.ptr, t.mdns, t.vni}
|
||||||
for _, ir := range il {
|
for _, ir := range il {
|
||||||
for _, ip := range ir.List() {
|
for _, ip := range ir.List() {
|
||||||
c, ok := ipMap[ip]
|
c, ok := ipMap[ip]
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ndpDiscover provides client discovery functionality using NDP protocol.
|
||||||
|
type ndpDiscover struct {
|
||||||
|
mac sync.Map // ip => mac
|
||||||
|
ip sync.Map // mac => ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh re-scans the NDP table.
|
||||||
|
func (nd *ndpDiscover) refresh() error {
|
||||||
|
nd.scan()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupIP returns the ipv6 associated with the input MAC address.
|
||||||
|
func (nd *ndpDiscover) LookupIP(mac string) string {
|
||||||
|
val, ok := nd.ip.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupMac returns the MAC address of the given IP address.
|
||||||
|
func (nd *ndpDiscover) LookupMac(ip string) string {
|
||||||
|
val, ok := nd.mac.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns human-readable format of ndpDiscover.
|
||||||
|
func (nd *ndpDiscover) String() string {
|
||||||
|
return "ndp"
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns all known IP addresses.
|
||||||
|
func (nd *ndpDiscover) List() []string {
|
||||||
|
if nd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var ips []string
|
||||||
|
nd.ip.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, value.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
nd.mac.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanWindows populates NDP table using information from "netsh" command.
|
||||||
|
func (nd *ndpDiscover) scanWindows(r io.Reader) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mac := parseMAC(fields[1]); mac != "" {
|
||||||
|
nd.mac.Store(fields[0], mac)
|
||||||
|
nd.ip.Store(mac, fields[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanUnix populates NDP table using information from "ndp" command.
|
||||||
|
func (nd *ndpDiscover) scanUnix(r io.Reader) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Scan() // skip header
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mac := parseMAC(fields[1]); mac != "" {
|
||||||
|
ip := fields[0]
|
||||||
|
if idx := strings.IndexByte(ip, '%'); idx != -1 {
|
||||||
|
ip = ip[:idx]
|
||||||
|
}
|
||||||
|
nd.mac.Store(ip, mac)
|
||||||
|
nd.ip.Store(mac, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeMac ensure the given MAC address have the proper format
|
||||||
|
// before being parsed.
|
||||||
|
//
|
||||||
|
// Example, changing "00:0:00:0:00:01" to "00:00:00:00:00:01", which
|
||||||
|
// can be seen on Darwin.
|
||||||
|
func normalizeMac(mac string) string {
|
||||||
|
if len(mac) == 17 {
|
||||||
|
return mac
|
||||||
|
}
|
||||||
|
// Windows use "-" instead of ":" as separator.
|
||||||
|
mac = strings.ReplaceAll(mac, "-", ":")
|
||||||
|
parts := strings.Split(mac, ":")
|
||||||
|
if len(parts) != 6 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for i, c := range parts {
|
||||||
|
if len(c) == 1 {
|
||||||
|
parts[i] = "0" + c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMAC parses the input MAC, doing normalization,
|
||||||
|
// and return the result after calling net.ParseMac function.
|
||||||
|
func parseMAC(mac string) string {
|
||||||
|
hw, _ := net.ParseMAC(normalizeMac(mac))
|
||||||
|
return hw.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// scan populates NDP table using information from system mappings.
|
||||||
|
func (nd *ndpDiscover) scan() {
|
||||||
|
neighs, err := netlink.NeighList(0, netlink.FAMILY_V6)
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Err(err).Msg("could not get neigh list")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range neighs {
|
||||||
|
ip := n.IP.String()
|
||||||
|
mac := n.HardwareAddr.String()
|
||||||
|
nd.mac.Store(ip, mac)
|
||||||
|
nd.ip.Store(mac, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// scan populates NDP table using information from system mappings.
|
||||||
|
func (nd *ndpDiscover) scan() {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
data, err := exec.Command("netsh", "interface", "ipv6", "show", "neighbors").Output()
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Err(err).Msg("could not query ndp table")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nd.scanWindows(bytes.NewReader(data))
|
||||||
|
default:
|
||||||
|
data, err := exec.Command("ndp", "-an").Output()
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Err(err).Msg("could not query ndp table")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nd.scanUnix(bytes.NewReader(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ndpDiscover_scanUnix(t *testing.T) {
|
||||||
|
r := strings.NewReader(`Neighbor Linklayer Address Netif Expire St Flgs Prbs
|
||||||
|
2405:4802:1f90:fda0:1459:ec89:523d:3583 00:0:00:0:00:01 en0 permanent R
|
||||||
|
2405:4802:1f90:fda0:186b:c54a:1370:c196 (incomplete) en0 expired N
|
||||||
|
2405:4802:1f90:fda0:88de:14ef:6a8c:579a 00:0:00:0:00:02 en0 permanent R
|
||||||
|
fe80::1%lo0 (incomplete) lo0 permanent R
|
||||||
|
`)
|
||||||
|
nd := &ndpDiscover{}
|
||||||
|
nd.scanUnix(r)
|
||||||
|
|
||||||
|
for _, m := range []*sync.Map{&nd.mac, &nd.ip} {
|
||||||
|
count := 0
|
||||||
|
m.Range(func(key, value any) bool {
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count != 2 {
|
||||||
|
t.Errorf("unexpected count, want 2, got: %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ndpDiscover_scanWindows(t *testing.T) {
|
||||||
|
r := strings.NewReader(`Interface 14: Wi-Fi
|
||||||
|
|
||||||
|
|
||||||
|
Internet Address Physical Address Type
|
||||||
|
-------------------------------------------- ----------------- -----------
|
||||||
|
2405:4802:1f90:fda0:ffff:ffff:ffff:ff88 00-00-00-00-00-00 Unreachable
|
||||||
|
fe80::1 60-57-47-21-dd-00 Reachable (Router)
|
||||||
|
fe80::6257:47ff:fe21:dd00 60-57-47-21-dd-00 Reachable (Router)
|
||||||
|
ff02::1 33-33-00-00-00-01 Permanent
|
||||||
|
ff02::2 33-33-00-00-00-02 Permanent
|
||||||
|
ff02::c 33-33-00-00-00-0c Permanent
|
||||||
|
`)
|
||||||
|
nd := &ndpDiscover{}
|
||||||
|
nd.scanWindows(r)
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
nd.mac.Range(func(key, value any) bool {
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count != 6 {
|
||||||
|
t.Errorf("unexpected count, want 6, got: %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
nd.ip.Range(func(key, value any) bool {
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count != 5 {
|
||||||
|
t.Errorf("unexpected count, want 5, got: %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user