Files
zeromesh/internal/vl2/switch.go

154 lines
3.1 KiB
Go

package vl2
import (
"log/slog"
"net"
"sync"
"time"
"zeromesh/internal/identity"
)
const (
MACTableExpiry = 5 * time.Minute
MACTableMaxSize = 4096
)
type MACEntry struct {
PeerAddr identity.Address
LastSeen time.Time
IsLocal bool
}
type PeerSender interface {
SendToPeer(peerAddr identity.Address, networkID uint32, frame []byte) error
BroadcastToPeers(networkID uint32, frame []byte, excludePeer identity.Address) error
}
type Switch struct {
networkID uint32
macTable map[MACKey]*MACEntry
mu sync.RWMutex
sender PeerSender
log *slog.Logger
}
func NewSwitch(networkID uint32, sender PeerSender, log *slog.Logger) *Switch {
return &Switch{
networkID: networkID,
macTable: make(map[MACKey]*MACEntry),
sender: sender,
log: log.With("component", "switch", "network", networkID),
}
}
func (sw *Switch) HandleLocalFrame(frame []byte) error {
parsed, err := ParseEthernetFrame(frame)
if err != nil {
return err
}
sw.learn(parsed.SrcMAC, identity.Address{}, true)
if parsed.IsBroadcast() || parsed.IsMulticast() {
return sw.sender.BroadcastToPeers(sw.networkID, frame, identity.Address{})
}
sw.mu.RLock()
entry, found := sw.macTable[MACToKey(parsed.DstMAC)]
sw.mu.RUnlock()
if found && !entry.IsLocal {
return sw.sender.SendToPeer(entry.PeerAddr, sw.networkID, frame)
}
if !found {
sw.log.Debug("unknown dst MAC, flooding", "dst", parsed.DstMAC)
return sw.sender.BroadcastToPeers(sw.networkID, frame, identity.Address{})
}
return nil
}
func (sw *Switch) HandleRemoteFrame(peerAddr identity.Address, frame []byte) ([]byte, error) {
parsed, err := ParseEthernetFrame(frame)
if err != nil {
return nil, err
}
sw.learn(parsed.SrcMAC, peerAddr, false)
if parsed.IsBroadcast() || parsed.IsMulticast() {
_ = sw.sender.BroadcastToPeers(sw.networkID, frame, peerAddr)
return frame, nil
}
sw.mu.RLock()
entry, found := sw.macTable[MACToKey(parsed.DstMAC)]
sw.mu.RUnlock()
if found && entry.IsLocal {
return frame, nil
}
if found && !entry.IsLocal {
_ = sw.sender.SendToPeer(entry.PeerAddr, sw.networkID, frame)
return nil, nil
}
_ = sw.sender.BroadcastToPeers(sw.networkID, frame, peerAddr)
return frame, nil
}
func (sw *Switch) learn(mac net.HardwareAddr, peerAddr identity.Address, isLocal bool) {
key := MACToKey(mac)
sw.mu.Lock()
defer sw.mu.Unlock()
if len(sw.macTable) >= MACTableMaxSize {
sw.evictOldest()
}
sw.macTable[key] = &MACEntry{
PeerAddr: peerAddr,
LastSeen: time.Now(),
IsLocal: isLocal,
}
}
func (sw *Switch) evictOldest() {
var oldestKey MACKey
var oldestTime time.Time
first := true
for k, v := range sw.macTable {
if first || v.LastSeen.Before(oldestTime) {
oldestKey = k
oldestTime = v.LastSeen
first = false
}
}
if !first {
delete(sw.macTable, oldestKey)
}
}
func (sw *Switch) CleanExpired() int {
sw.mu.Lock()
defer sw.mu.Unlock()
cutoff := time.Now().Add(-MACTableExpiry)
removed := 0
for k, v := range sw.macTable {
if v.LastSeen.Before(cutoff) && !v.IsLocal {
delete(sw.macTable, k)
removed++
}
}
return removed
}
func (sw *Switch) MACTableSize() int {
sw.mu.RLock()
defer sw.mu.RUnlock()
return len(sw.macTable)
}