mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
So when new clients join the network, ctrld can really the event and update client information to NDP table quickly.
220 lines
5.0 KiB
Go
220 lines
5.0 KiB
Go
package clientinfo
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mdlayher/ndp"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// listen listens on ipv6 link local for Neighbor Solicitation message
|
|
// to update new neighbors information to ndp table.
|
|
func (nd *ndpDiscover) listen(ctx context.Context) {
|
|
ifi, err := firstInterfaceWithV6LinkLocal()
|
|
if err != nil {
|
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("failed to find valid ipv6")
|
|
return
|
|
}
|
|
c, ip, err := ndp.Listen(ifi, ndp.LinkLocal)
|
|
if err != nil {
|
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("ndp listen failed")
|
|
return
|
|
}
|
|
defer c.Close()
|
|
ctrld.ProxyLogger.Load().Debug().Msgf("listening ndp on: %s", ip.String())
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
_ = c.SetReadDeadline(time.Now().Add(30 * time.Second))
|
|
msg, _, from, readErr := c.ReadFrom()
|
|
if readErr != nil {
|
|
var opErr *net.OpError
|
|
if errors.As(readErr, &opErr) && (opErr.Timeout() || opErr.Temporary()) {
|
|
continue
|
|
}
|
|
ctrld.ProxyLogger.Load().Debug().Err(readErr).Msg("ndp read loop error")
|
|
return
|
|
}
|
|
|
|
// Only looks for neighbor solicitation message, since new clients
|
|
// which join network will broadcast this message to us.
|
|
am, ok := msg.(*ndp.NeighborSolicitation)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fromIP := from.String()
|
|
for _, opt := range am.Options {
|
|
if lla, ok := opt.(*ndp.LinkLayerAddress); ok {
|
|
mac := lla.Addr.String()
|
|
nd.mac.Store(fromIP, mac)
|
|
nd.ip.Store(mac, fromIP)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
// firstInterfaceWithV6LinkLocal returns the first interface which is capable of using NDP.
|
|
func firstInterfaceWithV6LinkLocal() (*net.Interface, error) {
|
|
ifis, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ifi := range ifis {
|
|
// Skip if iface is down/loopback/non-multicast.
|
|
if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagLoopback != 0 || ifi.Flags&net.FlagMulticast == 0 {
|
|
continue
|
|
}
|
|
|
|
addrs, err := ifi.Addrs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
ipNet, ok := addr.(*net.IPNet)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ip, ok := netip.AddrFromSlice(ipNet.IP)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid ip address: %s", ipNet.String())
|
|
}
|
|
if ip.Is6() && !ip.Is4In6() {
|
|
return &ifi, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, errors.New("no interface can be used")
|
|
}
|