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) }