Files
Mellaris/engine/mac_resolver.go
hayzam 198f72814c
Some checks failed
Quality check / Static analysis (push) Has been cancelled
Quality check / Tests (push) Has been cancelled
engine: mac: more ipv6 gm
2026-02-11 13:18:34 +05:30

395 lines
8.3 KiB
Go

package engine
import (
"bufio"
"net"
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
const (
ifaceCacheTTL = 30 * time.Second
arpCacheTTL = 10 * time.Second
ndpCacheTTL = 10 * time.Second
)
type sourceMACResolver struct {
mu sync.RWMutex
lastIfaceRefresh time.Time
ifaceByIP map[string]net.HardwareAddr
lastARPRefresh time.Time
arpByIP map[string]net.HardwareAddr
lastNDPRefresh time.Time
ndpByIP map[string]net.HardwareAddr
}
func newSourceMACResolver() *sourceMACResolver {
return &sourceMACResolver{
ifaceByIP: make(map[string]net.HardwareAddr),
arpByIP: make(map[string]net.HardwareAddr),
ndpByIP: make(map[string]net.HardwareAddr),
}
}
func (r *sourceMACResolver) Resolve(ip net.IP) net.HardwareAddr {
if ip == nil {
return nil
}
ipKey := ip.String()
if ipKey == "" {
return nil
}
now := time.Now()
r.mu.RLock()
ifaceRefreshDue := now.Sub(r.lastIfaceRefresh) > ifaceCacheTTL
arpRefreshDue := now.Sub(r.lastARPRefresh) > arpCacheTTL
ndpRefreshDue := now.Sub(r.lastNDPRefresh) > ndpCacheTTL
if mac := r.ifaceByIP[ipKey]; len(mac) != 0 {
out := append(net.HardwareAddr(nil), mac...)
r.mu.RUnlock()
return out
}
if mac := r.arpByIP[ipKey]; len(mac) != 0 && !arpRefreshDue {
out := append(net.HardwareAddr(nil), mac...)
r.mu.RUnlock()
return out
}
if mac := r.ndpByIP[ipKey]; len(mac) != 0 && !ndpRefreshDue {
out := append(net.HardwareAddr(nil), mac...)
r.mu.RUnlock()
return out
}
r.mu.RUnlock()
if ifaceRefreshDue {
r.refreshIfaceCache(now)
}
if arpRefreshDue {
r.refreshARPCache(now)
}
if ndpRefreshDue {
r.refreshNDPCache(now)
}
r.mu.RLock()
defer r.mu.RUnlock()
if mac := r.ifaceByIP[ipKey]; len(mac) != 0 {
return append(net.HardwareAddr(nil), mac...)
}
if mac := r.arpByIP[ipKey]; len(mac) != 0 {
return append(net.HardwareAddr(nil), mac...)
}
if mac := r.ndpByIP[ipKey]; len(mac) != 0 {
return append(net.HardwareAddr(nil), mac...)
}
// On-demand IPv6 neighbor lookup via route-netlink as a last fast path.
if ip.To4() == nil {
if mac, ok := lookupNeighborMACNetlink(ip); ok {
out := append(net.HardwareAddr(nil), mac...)
r.mu.Lock()
r.ndpByIP[ipKey] = append(net.HardwareAddr(nil), mac...)
r.mu.Unlock()
return out
}
}
return nil
}
func (r *sourceMACResolver) refreshIfaceCache(now time.Time) {
interfaces, err := net.Interfaces()
if err != nil {
return
}
m := make(map[string]net.HardwareAddr)
for _, iface := range interfaces {
if len(iface.HardwareAddr) == 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok || ipNet.IP == nil {
continue
}
m[ipNet.IP.String()] = append(net.HardwareAddr(nil), iface.HardwareAddr...)
}
}
r.mu.Lock()
r.ifaceByIP = m
r.lastIfaceRefresh = now
r.mu.Unlock()
}
func (r *sourceMACResolver) refreshARPCache(now time.Time) {
f, err := os.Open("/proc/net/arp")
if err != nil {
return
}
defer f.Close()
m := make(map[string]net.HardwareAddr)
scanner := bufio.NewScanner(f)
lineNo := 0
for scanner.Scan() {
lineNo++
if lineNo == 1 {
continue // header
}
fields := strings.Fields(scanner.Text())
if len(fields) < 4 {
continue
}
ipStr := fields[0]
hwAddr := fields[3]
if hwAddr == "00:00:00:00:00:00" {
continue
}
mac, err := net.ParseMAC(hwAddr)
if err != nil {
continue
}
m[ipStr] = append(net.HardwareAddr(nil), mac...)
}
if err := scanner.Err(); err != nil {
return
}
r.mu.Lock()
r.arpByIP = m
r.lastARPRefresh = now
r.mu.Unlock()
}
func (r *sourceMACResolver) refreshNDPCache(now time.Time) {
m, ok := readNeighborCacheFile("/proc/net/ndisc_cache")
if !ok || len(m) == 0 {
// Fallback for environments without /proc/net/ndisc_cache.
m = readIPv6NeighNetlink()
}
if len(m) == 0 {
// Last-resort fallback for environments where route-netlink dumps are restricted.
m = readIPv6NeighCommand()
}
r.mu.Lock()
r.ndpByIP = m
r.lastNDPRefresh = now
r.mu.Unlock()
}
func readNeighborCacheFile(path string) (map[string]net.HardwareAddr, bool) {
f, err := os.Open(path)
if err != nil {
return nil, false
}
defer f.Close()
m := make(map[string]net.HardwareAddr)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
ip, mac, ok := parseNeighborLine(scanner.Text())
if !ok {
continue
}
m[ip] = mac
}
if err := scanner.Err(); err != nil {
return nil, false
}
return m, true
}
func readIPv6NeighNetlink() map[string]net.HardwareAddr {
const (
ndMsgLen = 12
ndaDst = 1
ndaLLAddr = 2
)
m := make(map[string]net.HardwareAddr)
conn, err := netlink.Dial(unix.NETLINK_ROUTE, nil)
if err != nil {
return m
}
defer conn.Close()
req := make([]byte, ndMsgLen)
req[0] = unix.AF_INET6
msgs, err := conn.Execute(netlink.Message{
Header: netlink.Header{
Type: unix.RTM_GETNEIGH,
Flags: netlink.Request | netlink.Dump,
},
Data: req,
})
if err != nil {
return m
}
for _, msg := range msgs {
if msg.Header.Type != unix.RTM_NEWNEIGH || len(msg.Data) < ndMsgLen || msg.Data[0] != unix.AF_INET6 {
continue
}
attrs, err := netlink.UnmarshalAttributes(msg.Data[ndMsgLen:])
if err != nil {
continue
}
var ipStr string
var mac net.HardwareAddr
for _, a := range attrs {
switch a.Type {
case ndaDst:
if len(a.Data) == net.IPv6len {
ipStr = net.IP(append([]byte(nil), a.Data...)).String()
}
case ndaLLAddr:
if len(a.Data) >= 6 {
candidate := append(net.HardwareAddr(nil), a.Data...)
if candidate.String() != "00:00:00:00:00:00" {
mac = candidate
}
}
}
}
if ipStr != "" && len(mac) != 0 {
m[ipStr] = mac
}
}
return m
}
func lookupNeighborMACNetlink(target net.IP) (net.HardwareAddr, bool) {
const (
ndMsgLen = 12
ndaDst = 1
ndaLLAddr = 2
)
if target == nil || target.To4() != nil {
return nil, false
}
target16 := target.To16()
if target16 == nil {
return nil, false
}
conn, err := netlink.Dial(unix.NETLINK_ROUTE, nil)
if err != nil {
return nil, false
}
defer conn.Close()
req := make([]byte, ndMsgLen)
req[0] = unix.AF_INET6
msgs, err := conn.Execute(netlink.Message{
Header: netlink.Header{
Type: unix.RTM_GETNEIGH,
Flags: netlink.Request | netlink.Dump,
},
Data: req,
})
if err != nil {
return nil, false
}
for _, msg := range msgs {
if msg.Header.Type != unix.RTM_NEWNEIGH || len(msg.Data) < ndMsgLen || msg.Data[0] != unix.AF_INET6 {
continue
}
attrs, err := netlink.UnmarshalAttributes(msg.Data[ndMsgLen:])
if err != nil {
continue
}
var dstIP net.IP
var mac net.HardwareAddr
for _, a := range attrs {
switch a.Type {
case ndaDst:
if len(a.Data) == net.IPv6len {
dstIP = net.IP(append([]byte(nil), a.Data...))
}
case ndaLLAddr:
if len(a.Data) >= 6 {
candidate := append(net.HardwareAddr(nil), a.Data...)
if candidate.String() != "00:00:00:00:00:00" {
mac = candidate
}
}
}
}
if dstIP != nil && mac != nil && dstIP.Equal(target16) {
return mac, true
}
}
return nil, false
}
func readIPv6NeighCommand() map[string]net.HardwareAddr {
commands := [][]string{
{"ip", "-6", "neigh", "show"},
{"/sbin/ip", "-6", "neigh", "show"},
{"/usr/sbin/ip", "-6", "neigh", "show"},
{"busybox", "ip", "-6", "neigh", "show"},
{"/bin/busybox", "ip", "-6", "neigh", "show"},
}
for _, cmd := range commands {
out, err := exec.Command(cmd[0], cmd[1:]...).Output()
if err != nil || len(out) == 0 {
continue
}
m := make(map[string]net.HardwareAddr)
for _, line := range strings.Split(string(out), "\n") {
ip, mac, ok := parseNeighborLine(line)
if !ok {
continue
}
m[ip] = mac
}
if len(m) != 0 {
return m
}
}
return map[string]net.HardwareAddr{}
}
func parseNeighborLine(line string) (string, net.HardwareAddr, bool) {
fields := strings.Fields(line)
if len(fields) < 2 {
return "", nil, false
}
var ipStr string
var mac net.HardwareAddr
for _, f := range fields {
if ip := net.ParseIP(f); ip != nil {
ipStr = ip.String()
continue
}
if parsedMAC, err := net.ParseMAC(f); err == nil {
// Ignore unresolved/bogus entries.
if parsedMAC.String() != "00:00:00:00:00:00" {
mac = append(net.HardwareAddr(nil), parsedMAC...)
}
}
}
if ipStr == "" || len(mac) == 0 {
return "", nil, false
}
return ipStr, mac, true
}