154 lines
3.1 KiB
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)
|
|
}
|