initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI
This commit is contained in:
153
internal/vl2/switch.go
Normal file
153
internal/vl2/switch.go
Normal file
@@ -0,0 +1,153 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user