Files
Mellaris/engine/mac_resolver.go
T
hayzam 7a3f6e945d Improves flow handling and adds runtime stats APIs
Refactors TCP and UDP flow managers to enhance analyzer selection and flow binding accuracy, including O(1) UDP stream rebinding by 5-tuple.
Introduces runtime stats tracking for engine and ruleset operations, exposing new APIs for granular performance and error metrics.
Optimizes GeoMatcher with result caching and supports efficient geosite set matching, reducing redundant computation in ruleset expressions.
2026-05-13 06:10:38 +05:30

394 lines
8.1 KiB
Go

//go:build linux
// +build linux
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"},
}
m := make(map[string]net.HardwareAddr)
for _, cmd := range commands {
out, err := exec.Command(cmd[0], cmd[1:]...).Output()
if err != nil || len(out) == 0 {
continue
}
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 m
}
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
}