initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI
This commit is contained in:
120
internal/vl2/arp.go
Normal file
120
internal/vl2/arp.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package vl2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log/slog"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ARPEntry struct {
|
||||
MAC net.HardwareAddr
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
type ARPProxy struct {
|
||||
cache map[string]*ARPEntry
|
||||
mu sync.RWMutex
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewARPProxy(log *slog.Logger) *ARPProxy {
|
||||
return &ARPProxy{
|
||||
cache: make(map[string]*ARPEntry),
|
||||
log: log.With("component", "arp"),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ARPProxy) Learn(ip net.IP, mac net.HardwareAddr) {
|
||||
key := ip.String()
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.cache[key] = &ARPEntry{
|
||||
MAC: mac,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ARPProxy) Lookup(ip net.IP) net.HardwareAddr {
|
||||
key := ip.String()
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
entry, ok := a.cache[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return entry.MAC
|
||||
}
|
||||
|
||||
func (a *ARPProxy) HandleARP(frame *EthernetFrame) []byte {
|
||||
if len(frame.Payload) < 28 {
|
||||
return nil
|
||||
}
|
||||
pl := frame.Payload
|
||||
|
||||
hrd := binary.BigEndian.Uint16(pl[0:2])
|
||||
pro := binary.BigEndian.Uint16(pl[2:4])
|
||||
hln := pl[4]
|
||||
pln := pl[5]
|
||||
op := binary.BigEndian.Uint16(pl[6:8])
|
||||
|
||||
if hrd != 1 || pro != EtherTypeIPv4 || hln != 6 || pln != 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
senderMAC := net.HardwareAddr(pl[8:14])
|
||||
senderIP := net.IP(pl[14:18])
|
||||
targetIP := net.IP(pl[24:28])
|
||||
|
||||
a.Learn(senderIP, senderMAC)
|
||||
|
||||
if op == 1 { // ARP request
|
||||
if localMAC := a.Lookup(targetIP); localMAC != nil {
|
||||
reply := make([]byte, 42)
|
||||
copy(reply[0:6], senderMAC)
|
||||
copy(reply[6:12], localMAC)
|
||||
binary.BigEndian.PutUint16(reply[12:14], EtherTypeARP)
|
||||
|
||||
reply[14] = 0x00
|
||||
reply[15] = 0x01
|
||||
binary.BigEndian.PutUint16(reply[16:18], EtherTypeIPv4)
|
||||
reply[18] = 6
|
||||
reply[19] = 4
|
||||
binary.BigEndian.PutUint16(reply[20:22], 2)
|
||||
|
||||
copy(reply[22:28], localMAC)
|
||||
copy(reply[28:32], targetIP)
|
||||
copy(reply[32:38], senderMAC)
|
||||
copy(reply[38:42], senderIP)
|
||||
|
||||
return reply
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ARPProxy) PeerFromARP(frame *EthernetFrame) (net.IP, net.HardwareAddr) {
|
||||
if len(frame.Payload) < 28 {
|
||||
return nil, nil
|
||||
}
|
||||
pl := frame.Payload
|
||||
hrd := binary.BigEndian.Uint16(pl[0:2])
|
||||
pro := binary.BigEndian.Uint16(pl[2:4])
|
||||
if hrd != 1 || pro != EtherTypeIPv4 {
|
||||
return nil, nil
|
||||
}
|
||||
return net.IP(pl[14:18]), net.HardwareAddr(pl[8:14])
|
||||
}
|
||||
|
||||
func (a *ARPProxy) CleanExpired() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
cutoff := time.Now().Add(-30 * time.Minute)
|
||||
for k, v := range a.cache {
|
||||
if v.LastSeen.Before(cutoff) {
|
||||
delete(a.cache, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
92
internal/vl2/frame.go
Normal file
92
internal/vl2/frame.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package vl2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
MinFrameSize = 14
|
||||
MaxFrameSize = 65535
|
||||
EtherTypeIPv4 = 0x0800
|
||||
EtherTypeARP = 0x0806
|
||||
EtherTypeIPv6 = 0x86DD
|
||||
)
|
||||
|
||||
type EthernetFrame struct {
|
||||
DstMAC net.HardwareAddr
|
||||
SrcMAC net.HardwareAddr
|
||||
EtherType uint16
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func ParseEthernetFrame(data []byte) (*EthernetFrame, error) {
|
||||
if len(data) < MinFrameSize {
|
||||
return nil, fmt.Errorf("frame too short: %d < %d", len(data), MinFrameSize)
|
||||
}
|
||||
return &EthernetFrame{
|
||||
DstMAC: net.HardwareAddr(data[0:6]),
|
||||
SrcMAC: net.HardwareAddr(data[6:12]),
|
||||
EtherType: binary.BigEndian.Uint16(data[12:14]),
|
||||
Payload: data[14:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *EthernetFrame) Encode() []byte {
|
||||
buf := make([]byte, MinFrameSize+len(f.Payload))
|
||||
copy(buf[0:6], f.DstMAC)
|
||||
copy(buf[6:12], f.SrcMAC)
|
||||
binary.BigEndian.PutUint16(buf[12:14], f.EtherType)
|
||||
copy(buf[14:], f.Payload)
|
||||
return buf
|
||||
}
|
||||
|
||||
func (f *EthernetFrame) IsBroadcast() bool {
|
||||
for _, b := range f.DstMAC {
|
||||
if b != 0xff {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *EthernetFrame) IsMulticast() bool {
|
||||
return len(f.DstMAC) > 0 && f.DstMAC[0]&0x01 == 0x01 && !f.IsBroadcast()
|
||||
}
|
||||
|
||||
func (f *EthernetFrame) IsARP() bool {
|
||||
return f.EtherType == EtherTypeARP
|
||||
}
|
||||
|
||||
func (f *EthernetFrame) IsIPv4() bool {
|
||||
return f.EtherType == EtherTypeIPv4
|
||||
}
|
||||
|
||||
func (f *EthernetFrame) IsIPv6() bool {
|
||||
return f.EtherType == EtherTypeIPv6
|
||||
}
|
||||
|
||||
type MACKey [6]byte
|
||||
|
||||
func MACToKey(mac net.HardwareAddr) MACKey {
|
||||
var key MACKey
|
||||
copy(key[:], mac)
|
||||
return key
|
||||
}
|
||||
|
||||
var frameBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := make([]byte, MaxFrameSize)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
func GetFrameBuf() *[]byte {
|
||||
return frameBufPool.Get().(*[]byte)
|
||||
}
|
||||
|
||||
func PutFrameBuf(buf *[]byte) {
|
||||
frameBufPool.Put(buf)
|
||||
}
|
||||
19
internal/vl2/mac.go
Normal file
19
internal/vl2/mac.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package vl2
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"zeromesh/internal/identity"
|
||||
)
|
||||
|
||||
// GenerateMAC creates a deterministic MAC address from network ID and node address.
|
||||
func GenerateMAC(networkID uint32, nodeAddr identity.Address) net.HardwareAddr {
|
||||
mac := make(net.HardwareAddr, 6)
|
||||
mac[0] = 0x02 // locally administered, unicast
|
||||
mac[1] = byte(networkID >> 16)
|
||||
mac[2] = byte(networkID >> 8)
|
||||
mac[3] = byte(networkID)
|
||||
mac[4] = nodeAddr[3]
|
||||
mac[5] = nodeAddr[4]
|
||||
return mac
|
||||
}
|
||||
38
internal/vl2/network.go
Normal file
38
internal/vl2/network.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package vl2
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"zeromesh/internal/identity"
|
||||
)
|
||||
|
||||
type NetworkConfig struct {
|
||||
ID uint32
|
||||
Name string
|
||||
IPRange string
|
||||
IP6Range string
|
||||
MTU int
|
||||
Multicast bool
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
Config NetworkConfig
|
||||
Switch *Switch
|
||||
ARP *ARPProxy
|
||||
LocalMAC [6]byte
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewNetwork(config NetworkConfig, nodeAddr identity.Address, sender PeerSender, log *slog.Logger) *Network {
|
||||
netLog := log.With("network", config.ID, "name", config.Name)
|
||||
mac := GenerateMAC(config.ID, nodeAddr)
|
||||
var macArr [6]byte
|
||||
copy(macArr[:], mac)
|
||||
return &Network{
|
||||
Config: config,
|
||||
Switch: NewSwitch(config.ID, sender, netLog),
|
||||
ARP: NewARPProxy(netLog),
|
||||
LocalMAC: macArr,
|
||||
log: netLog,
|
||||
}
|
||||
}
|
||||
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