From c1e6f5126a4b9aeb3757acaa0d6968e0ea095c48 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 3 Apr 2024 18:12:56 +0700 Subject: [PATCH] internal/clientinfo: watch NDP table changes on Linux So with clients which only use SLAAC, ctrld could see client's new ip as soon as its state changes to REACHABLE. Moreover, the NDP listener is also changed to listen on all possible ipv6 link local interfaces. That would allow ctrld to get all NDP events happening in local network. SLAAC RFC: https://datatracker.ietf.org/doc/html/rfc4862 --- internal/clientinfo/client_info.go | 1 + internal/clientinfo/ndp.go | 50 +++++++++++++++++++----------- internal/clientinfo/ndp_linux.go | 40 +++++++++++++++++++++++- internal/clientinfo/ndp_others.go | 5 +++ 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 1a775ea..225b9cb 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -224,6 +224,7 @@ func (t *Table) init() { cancel() }() go t.ndp.listen(ctx) + go t.ndp.subscribe(ctx) } // PTR lookup. if t.discoverPTR() { diff --git a/internal/clientinfo/ndp.go b/internal/clientinfo/ndp.go index 81edc66..0215254 100644 --- a/internal/clientinfo/ndp.go +++ b/internal/clientinfo/ndp.go @@ -70,16 +70,20 @@ func (nd *ndpDiscover) List() []string { } // saveInfo saves ip and mac info to mapping table. -// Last seen ip address will override the old one, -func (nd *ndpDiscover) saveInfo(ip, mac string) { +// If force is true, old ip will be removed before saving. +func (nd *ndpDiscover) saveInfo(ip, mac string, force bool) { + ip = normalizeIP(ip) // Store ip => map mapping, nd.mac.Store(ip, mac) - // If there is old ip => mac mapping, delete it. - old, ok := nd.ip.Load(mac) - if ok { - oldIP := old.(string) - nd.mac.Delete(oldIP) + + if force { + // If there is old ip => mac mapping, delete it. + if old, ok := nd.ip.Load(mac); ok { + oldIP := old.(string) + nd.mac.Delete(oldIP) + } } + // Store mac => ip mapping. nd.ip.Store(mac, ip) } @@ -87,12 +91,20 @@ func (nd *ndpDiscover) saveInfo(ip, mac string) { // 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() + ifis, err := allInterfacesWithV6LinkLocal() if err != nil { - ctrld.ProxyLogger.Load().Debug().Err(err).Msg("failed to find valid ipv6") + ctrld.ProxyLogger.Load().Debug().Err(err).Msg("failed to find valid ipv6 interfaces") return } - c, ip, err := ndp.Listen(ifi, ndp.LinkLocal) + for _, ifi := range ifis { + go func(ifi *net.Interface) { + nd.listenOnInterface(ctx, ifi) + }(ifi) + } +} + +func (nd *ndpDiscover) listenOnInterface(ctx context.Context, ifi *net.Interface) { + c, ip, err := ndp.Listen(ifi, ndp.Unspecified) if err != nil { ctrld.ProxyLogger.Load().Debug().Err(err).Msg("ndp listen failed") return @@ -126,7 +138,7 @@ func (nd *ndpDiscover) listen(ctx context.Context) { for _, opt := range am.Options { if lla, ok := opt.(*ndp.LinkLayerAddress); ok { mac := lla.Addr.String() - nd.saveInfo(fromIP, mac) + nd.saveInfo(fromIP, mac, true) } } } @@ -141,7 +153,7 @@ func (nd *ndpDiscover) scanWindows(r io.Reader) { continue } if mac := parseMAC(fields[1]); mac != "" { - nd.saveInfo(fields[0], mac) + nd.saveInfo(fields[0], mac, true) } } } @@ -160,7 +172,7 @@ func (nd *ndpDiscover) scanUnix(r io.Reader) { if idx := strings.IndexByte(ip, '%'); idx != -1 { ip = ip[:idx] } - nd.saveInfo(ip, mac) + nd.saveInfo(ip, mac, true) } } } @@ -195,14 +207,15 @@ func parseMAC(mac string) string { return hw.String() } -// firstInterfaceWithV6LinkLocal returns the first interface which is capable of using NDP. -func firstInterfaceWithV6LinkLocal() (*net.Interface, error) { +// allInterfacesWithV6LinkLocal returns all interfaces which is capable of using NDP. +func allInterfacesWithV6LinkLocal() ([]*net.Interface, error) { ifis, err := net.Interfaces() if err != nil { return nil, err } - + res := make([]*net.Interface, 0, len(ifis)) for _, ifi := range ifis { + ifi := ifi // 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 @@ -223,9 +236,10 @@ func firstInterfaceWithV6LinkLocal() (*net.Interface, error) { return nil, fmt.Errorf("invalid ip address: %s", ipNet.String()) } if ip.Is6() && !ip.Is4In6() { - return &ifi, nil + res = append(res, &ifi) + break } } } - return nil, errors.New("no interface can be used") + return res, nil } diff --git a/internal/clientinfo/ndp_linux.go b/internal/clientinfo/ndp_linux.go index ebffe0e..7b36ea5 100644 --- a/internal/clientinfo/ndp_linux.go +++ b/internal/clientinfo/ndp_linux.go @@ -1,7 +1,10 @@ package clientinfo import ( + "context" + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" "github.com/Control-D-Inc/ctrld" ) @@ -21,6 +24,41 @@ func (nd *ndpDiscover) scan() { } ip := n.IP.String() mac := n.HardwareAddr.String() - nd.saveInfo(ip, mac) + nd.saveInfo(ip, mac, false) + } +} + +// subscribe watches NDP table changes and update new information to local table. +func (nd *ndpDiscover) subscribe(ctx context.Context) { + ch := make(chan netlink.NeighUpdate) + done := make(chan struct{}) + defer close(done) + if err := netlink.NeighSubscribe(ch, done); err != nil { + ctrld.ProxyLogger.Load().Err(err).Msg("could not perform neighbor subscribing") + return + } + for { + select { + case <-ctx.Done(): + return + case nu := <-ch: + if nu.Family != netlink.FAMILY_V6 { + continue + } + ip := normalizeIP(nu.IP.String()) + if nu.Type == unix.RTM_DELNEIGH { + ctrld.ProxyLogger.Load().Debug().Msgf("removing NDP neighbor: %s", ip) + nd.mac.Delete(ip) + continue + } + mac := nu.HardwareAddr.String() + switch nu.State { + case netlink.NUD_REACHABLE: + nd.saveInfo(ip, mac, false) + case netlink.NUD_FAILED: + ctrld.ProxyLogger.Load().Debug().Msgf("removing NDP neighbor with failed state: %s", ip) + nd.mac.Delete(ip) + } + } } } diff --git a/internal/clientinfo/ndp_others.go b/internal/clientinfo/ndp_others.go index 05ac322..007407b 100644 --- a/internal/clientinfo/ndp_others.go +++ b/internal/clientinfo/ndp_others.go @@ -4,6 +4,7 @@ package clientinfo import ( "bytes" + "context" "os/exec" "runtime" @@ -29,3 +30,7 @@ func (nd *ndpDiscover) scan() { nd.scanUnix(bytes.NewReader(data)) } } + +// subscribe watches NDP table changes and update new information to local table. +// This is a stub method, and only works on Linux at this moment. +func (nd *ndpDiscover) subscribe(ctx context.Context) {}