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) {}