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
This commit is contained in:
Cuong Manh Le
2024-04-04 22:44:25 +07:00
committed by Cuong Manh Le
parent 1a8c1ec73d
commit c1e6f5126a
4 changed files with 77 additions and 19 deletions
+1
View File
@@ -224,6 +224,7 @@ func (t *Table) init() {
cancel() cancel()
}() }()
go t.ndp.listen(ctx) go t.ndp.listen(ctx)
go t.ndp.subscribe(ctx)
} }
// PTR lookup. // PTR lookup.
if t.discoverPTR() { if t.discoverPTR() {
+32 -18
View File
@@ -70,16 +70,20 @@ func (nd *ndpDiscover) List() []string {
} }
// saveInfo saves ip and mac info to mapping table. // saveInfo saves ip and mac info to mapping table.
// Last seen ip address will override the old one, // If force is true, old ip will be removed before saving.
func (nd *ndpDiscover) saveInfo(ip, mac string) { func (nd *ndpDiscover) saveInfo(ip, mac string, force bool) {
ip = normalizeIP(ip)
// Store ip => map mapping, // Store ip => map mapping,
nd.mac.Store(ip, mac) nd.mac.Store(ip, mac)
// If there is old ip => mac mapping, delete it.
old, ok := nd.ip.Load(mac) if force {
if ok { // If there is old ip => mac mapping, delete it.
oldIP := old.(string) if old, ok := nd.ip.Load(mac); ok {
nd.mac.Delete(oldIP) oldIP := old.(string)
nd.mac.Delete(oldIP)
}
} }
// Store mac => ip mapping. // Store mac => ip mapping.
nd.ip.Store(mac, ip) 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 // listen listens on ipv6 link local for Neighbor Solicitation message
// to update new neighbors information to ndp table. // to update new neighbors information to ndp table.
func (nd *ndpDiscover) listen(ctx context.Context) { func (nd *ndpDiscover) listen(ctx context.Context) {
ifi, err := firstInterfaceWithV6LinkLocal() ifis, err := allInterfacesWithV6LinkLocal()
if err != nil { 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 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 { if err != nil {
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("ndp listen failed") ctrld.ProxyLogger.Load().Debug().Err(err).Msg("ndp listen failed")
return return
@@ -126,7 +138,7 @@ func (nd *ndpDiscover) listen(ctx context.Context) {
for _, opt := range am.Options { for _, opt := range am.Options {
if lla, ok := opt.(*ndp.LinkLayerAddress); ok { if lla, ok := opt.(*ndp.LinkLayerAddress); ok {
mac := lla.Addr.String() 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 continue
} }
if mac := parseMAC(fields[1]); mac != "" { 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 { if idx := strings.IndexByte(ip, '%'); idx != -1 {
ip = ip[:idx] ip = ip[:idx]
} }
nd.saveInfo(ip, mac) nd.saveInfo(ip, mac, true)
} }
} }
} }
@@ -195,14 +207,15 @@ func parseMAC(mac string) string {
return hw.String() return hw.String()
} }
// firstInterfaceWithV6LinkLocal returns the first interface which is capable of using NDP. // allInterfacesWithV6LinkLocal returns all interfaces which is capable of using NDP.
func firstInterfaceWithV6LinkLocal() (*net.Interface, error) { func allInterfacesWithV6LinkLocal() ([]*net.Interface, error) {
ifis, err := net.Interfaces() ifis, err := net.Interfaces()
if err != nil { if err != nil {
return nil, err return nil, err
} }
res := make([]*net.Interface, 0, len(ifis))
for _, ifi := range ifis { for _, ifi := range ifis {
ifi := ifi
// Skip if iface is down/loopback/non-multicast. // Skip if iface is down/loopback/non-multicast.
if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagLoopback != 0 || ifi.Flags&net.FlagMulticast == 0 { if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagLoopback != 0 || ifi.Flags&net.FlagMulticast == 0 {
continue continue
@@ -223,9 +236,10 @@ func firstInterfaceWithV6LinkLocal() (*net.Interface, error) {
return nil, fmt.Errorf("invalid ip address: %s", ipNet.String()) return nil, fmt.Errorf("invalid ip address: %s", ipNet.String())
} }
if ip.Is6() && !ip.Is4In6() { 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
} }
+39 -1
View File
@@ -1,7 +1,10 @@
package clientinfo package clientinfo
import ( import (
"context"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
) )
@@ -21,6 +24,41 @@ func (nd *ndpDiscover) scan() {
} }
ip := n.IP.String() ip := n.IP.String()
mac := n.HardwareAddr.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)
}
}
} }
} }
+5
View File
@@ -4,6 +4,7 @@ package clientinfo
import ( import (
"bytes" "bytes"
"context"
"os/exec" "os/exec"
"runtime" "runtime"
@@ -29,3 +30,7 @@ func (nd *ndpDiscover) scan() {
nd.scanUnix(bytes.NewReader(data)) 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) {}