mac resolution
Some checks failed
Quality check / Tests (push) Has been cancelled
Quality check / Static analysis (push) Has been cancelled

This commit is contained in:
2026-02-11 12:04:11 +05:30
parent 94e1e26cc3
commit a879ab4140
7 changed files with 220 additions and 20 deletions

View File

@@ -2,6 +2,7 @@ package engine
import ( import (
"context" "context"
"encoding/binary"
"runtime" "runtime"
"git.difuse.io/Difuse/Mellaris/io" "git.difuse.io/Difuse/Mellaris/io"
@@ -24,6 +25,7 @@ func NewEngine(config Config) (Engine, error) {
if workerCount <= 0 { if workerCount <= 0 {
workerCount = runtime.NumCPU() workerCount = runtime.NumCPU()
} }
macResolver := newSourceMACResolver()
var err error var err error
workers := make([]*worker, workerCount) workers := make([]*worker, workerCount)
for i := range workers { for i := range workers {
@@ -32,6 +34,7 @@ func NewEngine(config Config) (Engine, error) {
ChanSize: config.WorkerQueueSize, ChanSize: config.WorkerQueueSize,
Logger: config.Logger, Logger: config.Logger,
Ruleset: config.Ruleset, Ruleset: config.Ruleset,
MACResolver: macResolver,
TCPMaxBufferedPagesTotal: config.WorkerTCPMaxBufferedPagesTotal, TCPMaxBufferedPagesTotal: config.WorkerTCPMaxBufferedPagesTotal,
TCPMaxBufferedPagesPerConn: config.WorkerTCPMaxBufferedPagesPerConn, TCPMaxBufferedPagesPerConn: config.WorkerTCPMaxBufferedPagesPerConn,
UDPMaxStreams: config.WorkerUDPMaxStreams, UDPMaxStreams: config.WorkerUDPMaxStreams,
@@ -90,13 +93,8 @@ func (e *engine) Run(ctx context.Context) error {
// dispatch dispatches a packet to a worker. // dispatch dispatches a packet to a worker.
func (e *engine) dispatch(p io.Packet) bool { func (e *engine) dispatch(p io.Packet) bool {
data := p.Data() data := p.Data()
ipVersion := data[0] >> 4 layerType, srcMAC, dstMAC, ok := classifyPacket(data)
var layerType gopacket.LayerType if !ok {
if ipVersion == 4 {
layerType = layers.LayerTypeIPv4
} else if ipVersion == 6 {
layerType = layers.LayerTypeIPv6
} else {
// Unsupported network layer // Unsupported network layer
_ = e.io.SetVerdict(p, io.VerdictAcceptStream, nil) _ = e.io.SetVerdict(p, io.VerdictAcceptStream, nil)
return true return true
@@ -107,9 +105,41 @@ func (e *engine) dispatch(p io.Packet) bool {
e.workers[index].Feed(&workerPacket{ e.workers[index].Feed(&workerPacket{
StreamID: p.StreamID(), StreamID: p.StreamID(),
Packet: packet, Packet: packet,
SrcMAC: srcMAC,
DstMAC: dstMAC,
SetVerdict: func(v io.Verdict, b []byte) error { SetVerdict: func(v io.Verdict, b []byte) error {
return e.io.SetVerdict(p, v, b) return e.io.SetVerdict(p, v, b)
}, },
}) })
return true return true
} }
// classifyPacket detects packet framing and returns a gopacket decode layer
// plus best-effort source/destination MAC addresses when available.
func classifyPacket(data []byte) (gopacket.LayerType, []byte, []byte, bool) {
if len(data) == 0 {
return 0, nil, nil, false
}
// Fast path for IP packets (NFQUEUE payloads are typically IP-only).
ipVersion := data[0] >> 4
if ipVersion == 4 {
return layers.LayerTypeIPv4, nil, nil, true
}
if ipVersion == 6 {
return layers.LayerTypeIPv6, nil, nil, true
}
// Ethernet frame path (for custom PacketIO implementations).
if len(data) >= 14 {
etherType := binary.BigEndian.Uint16(data[12:14])
if etherType == uint16(layers.EthernetTypeIPv4) || etherType == uint16(layers.EthernetTypeIPv6) {
return layers.LayerTypeEthernet,
append([]byte(nil), data[6:12]...),
append([]byte(nil), data[:6]...),
true
}
}
return 0, nil, nil, false
}

145
engine/mac_resolver.go Normal file
View File

@@ -0,0 +1,145 @@
package engine
import (
"bufio"
"net"
"os"
"strings"
"sync"
"time"
)
const (
ifaceCacheTTL = 30 * time.Second
arpCacheTTL = 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
}
func newSourceMACResolver() *sourceMACResolver {
return &sourceMACResolver{
ifaceByIP: make(map[string]net.HardwareAddr),
arpByIP: 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
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
}
r.mu.RUnlock()
if ifaceRefreshDue {
r.refreshIfaceCache(now)
}
if arpRefreshDue {
r.refreshARPCache(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...)
}
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()
}

View File

@@ -27,7 +27,8 @@ const (
type tcpContext struct { type tcpContext struct {
*gopacket.PacketMetadata *gopacket.PacketMetadata
Verdict tcpVerdict Verdict tcpVerdict
SrcMAC, DstMAC net.HardwareAddr
} }
func (ctx *tcpContext) GetCaptureInfo() gopacket.CaptureInfo { func (ctx *tcpContext) GetCaptureInfo() gopacket.CaptureInfo {
@@ -46,9 +47,12 @@ type tcpStreamFactory struct {
func (f *tcpStreamFactory) New(ipFlow, tcpFlow gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream { func (f *tcpStreamFactory) New(ipFlow, tcpFlow gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
id := f.Node.Generate() id := f.Node.Generate()
ipSrc, ipDst := net.IP(ipFlow.Src().Raw()), net.IP(ipFlow.Dst().Raw()) ipSrc, ipDst := net.IP(ipFlow.Src().Raw()), net.IP(ipFlow.Dst().Raw())
ctx := ac.(*tcpContext)
info := ruleset.StreamInfo{ info := ruleset.StreamInfo{
ID: id.Int64(), ID: id.Int64(),
Protocol: ruleset.ProtocolTCP, Protocol: ruleset.ProtocolTCP,
SrcMAC: append(net.HardwareAddr(nil), ctx.SrcMAC...),
DstMAC: append(net.HardwareAddr(nil), ctx.DstMAC...),
SrcIP: ipSrc, SrcIP: ipSrc,
DstIP: ipDst, DstIP: ipDst,
SrcPort: uint16(tcp.SrcPort), SrcPort: uint16(tcp.SrcPort),

View File

@@ -31,8 +31,9 @@ const (
var errInvalidModifier = errors.New("invalid modifier") var errInvalidModifier = errors.New("invalid modifier")
type udpContext struct { type udpContext struct {
Verdict udpVerdict Verdict udpVerdict
Packet []byte Packet []byte
SrcMAC, DstMAC net.HardwareAddr
} }
type udpStreamFactory struct { type udpStreamFactory struct {
@@ -50,6 +51,8 @@ func (f *udpStreamFactory) New(ipFlow, udpFlow gopacket.Flow, udp *layers.UDP, u
info := ruleset.StreamInfo{ info := ruleset.StreamInfo{
ID: id.Int64(), ID: id.Int64(),
Protocol: ruleset.ProtocolUDP, Protocol: ruleset.ProtocolUDP,
SrcMAC: append(net.HardwareAddr(nil), uc.SrcMAC...),
DstMAC: append(net.HardwareAddr(nil), uc.DstMAC...),
SrcIP: ipSrc, SrcIP: ipSrc,
DstIP: ipDst, DstIP: ipDst,
SrcPort: uint16(udp.SrcPort), SrcPort: uint16(udp.SrcPort),

View File

@@ -2,6 +2,7 @@ package engine
import ( import (
"context" "context"
"net"
"git.difuse.io/Difuse/Mellaris/io" "git.difuse.io/Difuse/Mellaris/io"
"git.difuse.io/Difuse/Mellaris/ruleset" "git.difuse.io/Difuse/Mellaris/ruleset"
@@ -22,13 +23,16 @@ const (
type workerPacket struct { type workerPacket struct {
StreamID uint32 StreamID uint32
Packet gopacket.Packet Packet gopacket.Packet
SrcMAC net.HardwareAddr
DstMAC net.HardwareAddr
SetVerdict func(io.Verdict, []byte) error SetVerdict func(io.Verdict, []byte) error
} }
type worker struct { type worker struct {
id int id int
packetChan chan *workerPacket packetChan chan *workerPacket
logger Logger logger Logger
macResolver *sourceMACResolver
tcpStreamFactory *tcpStreamFactory tcpStreamFactory *tcpStreamFactory
tcpStreamPool *reassembly.StreamPool tcpStreamPool *reassembly.StreamPool
@@ -45,6 +49,7 @@ type workerConfig struct {
ChanSize int ChanSize int
Logger Logger Logger Logger
Ruleset ruleset.Ruleset Ruleset ruleset.Ruleset
MACResolver *sourceMACResolver
TCPMaxBufferedPagesTotal int TCPMaxBufferedPagesTotal int
TCPMaxBufferedPagesPerConn int TCPMaxBufferedPagesPerConn int
UDPMaxStreams int UDPMaxStreams int
@@ -95,6 +100,7 @@ func newWorker(config workerConfig) (*worker, error) {
id: config.ID, id: config.ID,
packetChan: make(chan *workerPacket, config.ChanSize), packetChan: make(chan *workerPacket, config.ChanSize),
logger: config.Logger, logger: config.Logger,
macResolver: config.MACResolver,
tcpStreamFactory: tcpSF, tcpStreamFactory: tcpSF,
tcpStreamPool: tcpStreamPool, tcpStreamPool: tcpStreamPool,
tcpAssembler: tcpAssembler, tcpAssembler: tcpAssembler,
@@ -120,7 +126,7 @@ func (w *worker) Run(ctx context.Context) {
// Closed // Closed
return return
} }
v, b := w.handle(wPkt.StreamID, wPkt.Packet) v, b := w.handle(wPkt.StreamID, wPkt.Packet, wPkt.SrcMAC, wPkt.DstMAC)
_ = wPkt.SetVerdict(v, b) _ = wPkt.SetVerdict(v, b)
} }
} }
@@ -133,18 +139,21 @@ func (w *worker) UpdateRuleset(r ruleset.Ruleset) error {
return w.udpStreamFactory.UpdateRuleset(r) return w.udpStreamFactory.UpdateRuleset(r)
} }
func (w *worker) handle(streamID uint32, p gopacket.Packet) (io.Verdict, []byte) { func (w *worker) handle(streamID uint32, p gopacket.Packet, srcMAC, dstMAC net.HardwareAddr) (io.Verdict, []byte) {
netLayer, trLayer := p.NetworkLayer(), p.TransportLayer() netLayer, trLayer := p.NetworkLayer(), p.TransportLayer()
if netLayer == nil || trLayer == nil { if netLayer == nil || trLayer == nil {
// Invalid packet // Invalid packet
return io.VerdictAccept, nil return io.VerdictAccept, nil
} }
ipFlow := netLayer.NetworkFlow() ipFlow := netLayer.NetworkFlow()
if len(srcMAC) == 0 && w.macResolver != nil {
srcMAC = w.macResolver.Resolve(net.IP(ipFlow.Src().Raw()))
}
switch tr := trLayer.(type) { switch tr := trLayer.(type) {
case *layers.TCP: case *layers.TCP:
return w.handleTCP(ipFlow, p.Metadata(), tr), nil return w.handleTCP(ipFlow, srcMAC, dstMAC, p.Metadata(), tr), nil
case *layers.UDP: case *layers.UDP:
v, modPayload := w.handleUDP(streamID, ipFlow, tr) v, modPayload := w.handleUDP(streamID, ipFlow, srcMAC, dstMAC, tr)
if v == io.VerdictAcceptModify && modPayload != nil { if v == io.VerdictAcceptModify && modPayload != nil {
tr.Payload = modPayload tr.Payload = modPayload
_ = tr.SetNetworkLayerForChecksum(netLayer) _ = tr.SetNetworkLayerForChecksum(netLayer)
@@ -167,18 +176,22 @@ func (w *worker) handle(streamID uint32, p gopacket.Packet) (io.Verdict, []byte)
} }
} }
func (w *worker) handleTCP(ipFlow gopacket.Flow, pMeta *gopacket.PacketMetadata, tcp *layers.TCP) io.Verdict { func (w *worker) handleTCP(ipFlow gopacket.Flow, srcMAC, dstMAC net.HardwareAddr, pMeta *gopacket.PacketMetadata, tcp *layers.TCP) io.Verdict {
ctx := &tcpContext{ ctx := &tcpContext{
PacketMetadata: pMeta, PacketMetadata: pMeta,
Verdict: tcpVerdictAccept, Verdict: tcpVerdictAccept,
SrcMAC: srcMAC,
DstMAC: dstMAC,
} }
w.tcpAssembler.AssembleWithContext(ipFlow, tcp, ctx) w.tcpAssembler.AssembleWithContext(ipFlow, tcp, ctx)
return io.Verdict(ctx.Verdict) return io.Verdict(ctx.Verdict)
} }
func (w *worker) handleUDP(streamID uint32, ipFlow gopacket.Flow, udp *layers.UDP) (io.Verdict, []byte) { func (w *worker) handleUDP(streamID uint32, ipFlow gopacket.Flow, srcMAC, dstMAC net.HardwareAddr, udp *layers.UDP) (io.Verdict, []byte) {
ctx := &udpContext{ ctx := &udpContext{
Verdict: udpVerdictAccept, Verdict: udpVerdictAccept,
SrcMAC: srcMAC,
DstMAC: dstMAC,
} }
w.udpStreamManager.MatchWithContext(streamID, ipFlow, udp, ctx) w.udpStreamManager.MatchWithContext(streamID, ipFlow, udp, ctx)
return io.Verdict(ctx.Verdict), ctx.Packet return io.Verdict(ctx.Verdict), ctx.Packet

View File

@@ -191,6 +191,10 @@ func streamInfoToExprEnv(info StreamInfo) map[string]interface{} {
m := map[string]interface{}{ m := map[string]interface{}{
"id": info.ID, "id": info.ID,
"proto": info.Protocol.String(), "proto": info.Protocol.String(),
"mac": map[string]string{
"src": info.SrcMAC.String(),
"dst": info.DstMAC.String(),
},
"ip": map[string]string{ "ip": map[string]string{
"src": info.SrcIP.String(), "src": info.SrcIP.String(),
"dst": info.DstIP.String(), "dst": info.DstIP.String(),
@@ -211,7 +215,7 @@ func streamInfoToExprEnv(info StreamInfo) map[string]interface{} {
func isBuiltInAnalyzer(name string) bool { func isBuiltInAnalyzer(name string) bool {
switch name { switch name {
case "id", "proto", "ip", "port": case "id", "proto", "mac", "ip", "port":
return true return true
default: default:
return false return false

View File

@@ -67,6 +67,7 @@ const (
type StreamInfo struct { type StreamInfo struct {
ID int64 ID int64
Protocol Protocol Protocol Protocol
SrcMAC, DstMAC net.HardwareAddr
SrcIP, DstIP net.IP SrcIP, DstIP net.IP
SrcPort, DstPort uint16 SrcPort, DstPort uint16
Props analyzer.CombinedPropMap Props analyzer.CombinedPropMap