Skip to content

Frame Processing

The internal/frame/ module provides low-level Ethernet frame processing for TAP device networking. This module is essential for Layer 2 mesh networking, enabling virtual LAN functionality between peers.

The frame processing system provides:

  • Ethernet Frame Parsing - Parse raw Ethernet frames from TAP devices
  • Frame Building - Construct Ethernet frames for transmission
  • ARP Handling - Process ARP requests/replies for address resolution
  • MAC Table Management - Learn and lookup MAC addresses for forwarding decisions
ComponentFilePurpose
EthernetFrameethernet.goEthernet frame parsing and building
ARPPacketarp.goARP packet handling and ARP interceptor
MACTablemac_table.goThread-safe MAC address learning and lookup

The Ethernet frame follows the IEEE 802.3 standard:

Implementation details (internal/frame/ethernet.go):

// EthernetHeader represents an Ethernet frame header
type EthernetHeader struct {
DstMAC net.HardwareAddr // Destination MAC address (6 bytes)
SrcMAC net.HardwareAddr // Source MAC address (6 bytes)
EtherType EtherType // EtherType field (2 bytes)
}
// EthernetFrame represents a complete Ethernet frame
type EthernetFrame struct {
Header EthernetHeader
Payload []byte
Raw []byte // Original raw frame (for forwarding)
}
ConstantValueDescription
EthernetHeaderSize14 bytesHeader size without payload
MinEthernetFrame60 bytesMinimum frame size (padded if smaller)
MaxEthernetFrame1522 bytesMaximum with VLAN tag
MaxPayload1500 bytesStandard MTU
EtherTypeValueDescription
EtherTypeIPv40x0800IPv4 packets
EtherTypeARP0x0806ARP packets
EtherTypeIPv60x86DDIPv6 packets
EtherTypeVLAN0x8100802.1Q VLAN-tagged frames
// Parse a raw Ethernet frame from TAP device
frame, err := frame.ParseEthernetFrame(rawBytes)
if err != nil {
// Handle: ErrFrameTooShort, etc.
}
// Access frame fields
destMAC := frame.Header.DstMAC
srcMAC := frame.Header.SrcMAC
etherType := frame.Header.EtherType
payload := frame.Payload
// Check frame type
if frame.IsIPv4() {
// Process IPv4 packet
} else if frame.IsARP() {
// Process ARP packet
}
// Check destination type
if frame.IsBroadcast() {
// Flood to all peers
} else if frame.IsMulticast() {
// Send to multicast group
} else {
// Unicast - lookup MAC table
}
// Build an Ethernet frame
frameBytes, err := frame.BuildEthernetFrame(
dstMAC, // Destination MAC
srcMAC, // Source MAC
frame.EtherTypeIPv4, // EtherType
ipPayload, // IP packet payload
)
if err != nil {
// Handle: ErrInvalidMAC, ErrPayloadTooLarge
}
// Write to TAP device
_, err = tapDevice.Write(frameBytes)

Implementation details (internal/frame/arp.go):

// ARPPacket represents an ARP packet
type ARPPacket struct {
HardwareType uint16 // Ethernet = 1
ProtocolType uint16 // IPv4 = 0x0800
HardwareAddrLen uint8 // 6 for Ethernet
ProtocolAddrLen uint8 // 4 for IPv4
Operation uint16 // 1 = request, 2 = reply
SenderHardwareAddr net.HardwareAddr
SenderProtocolAddr netip.Addr
TargetHardwareAddr net.HardwareAddr
TargetProtocolAddr netip.Addr
}
OperationValueDescription
ARPRequest1Who has IP X? Tell IP Y
ARPReply2IP X is at MAC Z

The ARPInterceptor handles ARP traffic for virtual network interfaces:

// Create ARP interceptor for a virtual interface
interceptor := frame.NewARPInterceptor(
localMAC, // Our virtual MAC address
localIP, // Our virtual IP address
macTable, // MAC table for learning
)
// Process incoming frame
response := interceptor.HandleFrame(frameData)
if response != nil {
// Write ARP reply to TAP device
tapDevice.Write(response)
}
// Build ARP request
arpRequest, _ := frame.BuildARPRequest(
senderMAC, // Our MAC
senderIP, // Our IP
targetIP, // IP we're looking for
)
// Build ARP reply
arpReply, _ := frame.BuildARPReply(
senderMAC, // Our MAC
senderIP, // Our IP
targetMAC, // Requester's MAC
targetIP, // Requester's IP
)
// Build complete Ethernet frame with ARP packet
ethFrame, _ := frame.BuildARPRequestFrame(senderMAC, senderIP, targetIP)
// Automatically uses broadcast destination for requests

The MAC table maintains a mapping between:

  • MAC addresses seen on the virtual network
  • Peer IDs that own those MAC addresses
  • Optional virtual IP addresses

This enables the mesh node to forward unicast frames directly to the correct peer.

type MACEntry struct {
MAC net.HardwareAddr // MAC address
PeerID string // Peer that owns this MAC
VirtualIP netip.Addr // Associated IP (if known)
LastSeen time.Time // Last traffic timestamp
LearnedAt time.Time // When first learned
Static bool // Static entries don't expire
}

Implementation details (internal/frame/mac_table.go):

OperationDescription
Learn(mac, peerID)Add or update a MAC entry
LearnWithIP(mac, peerID, ip)Learn with associated IP
LearnStatic(mac, peerID, ip)Add non-expiring entry
Lookup(mac)Get peer ID for MAC
LookupEntry(mac)Get full entry for MAC
LookupByIP(ip)Find MAC by IP address
GetPeerMACs(peerID)Get all MACs for a peer
Remove(mac)Remove specific entry
RemovePeer(peerID)Remove all entries for peer
Expire()Remove stale entries

Entries automatically expire after the configured MaxAge (default: 5 minutes):

// Configure MAC table
cfg := frame.MACTableConfig{
MaxAge: 5 * time.Minute,
}
table := frame.NewMACTable(cfg)
// Start background expiry worker
stopCh := make(chan struct{})
table.StartExpiryWorker(time.Minute, stopCh)
// Manual expiration
expired := table.Expire() // Returns count of expired entries

The MAC table is fully thread-safe:

  • Uses sync.RWMutex for concurrent access
  • Read operations use RLock() for parallelism
  • Write operations use Lock() for exclusivity

The MeshNode (internal/mesh/node.go) integrates the frame module:

// MeshNode uses frame processing for TAP networking
type MeshNode struct {
// ...
macTable *frame.MACTable
arpHandler *frame.ARPInterceptor
// ...
}
// Initialization
func (n *MeshNode) initializeDevice() error {
// Create MAC table
n.macTable = frame.NewMACTable(frame.MACTableConfig{
MaxAge: 5 * time.Minute,
})
// Create ARP handler for TAP mode
if n.config.Device.Type == "tap" && n.localMAC != nil {
n.arpHandler = frame.NewARPInterceptor(
n.localMAC,
n.localIP,
n.macTable,
)
}
// ...
}
// handleTAPFrame processes frames from TAP device
func (n *MeshNode) handleTAPFrame(frameData []byte) {
// Parse Ethernet frame
ethFrame, err := frame.ParseEthernetFrame(frameData)
if err != nil {
return
}
// Learn source MAC
n.macTable.Learn(ethFrame.Header.SrcMAC, n.localPeerID)
// Handle ARP
if ethFrame.Header.EtherType == frame.EtherTypeARP {
response := n.arpHandler.HandleFrame(frameData)
if response != nil {
n.device.Write(response)
}
return
}
// Route based on destination
if frame.IsBroadcast(ethFrame.Header.DstMAC) {
n.broadcast.Broadcast(frameData, 8)
} else if frame.IsMulticast(ethFrame.Header.DstMAC) {
n.broadcast.Multicast(groupID, frameData, 8)
} else {
// Unicast - lookup and send
entry, found := n.macTable.LookupEntry(ethFrame.Header.DstMAC)
if found {
n.sendToP2P(entry.PeerID, frameData)
} else {
n.broadcast.Broadcast(frameData, 8) // Unknown - flood
}
}
}

You can extend the frame processing by implementing custom handlers:

// Custom frame handler interface (conceptual)
type FrameHandler interface {
HandleFrame(frame *frame.EthernetFrame) (response []byte, handled bool)
}
// Example: Custom protocol handler
type MyProtocolHandler struct {
etherType frame.EtherType
}
func (h *MyProtocolHandler) HandleFrame(f *frame.EthernetFrame) ([]byte, bool) {
if f.Header.EtherType != h.etherType {
return nil, false
}
// Process custom protocol
return response, true
}

Implement custom lookups using the Find method:

// Find all MACs for a specific IP subnet
subnetMACs := macTable.Find(func(e *frame.MACEntry) bool {
if !e.VirtualIP.IsValid() {
return false
}
prefix := netip.MustParsePrefix("10.100.0.0/24")
return prefix.Contains(e.VirtualIP)
})
// Find recently active MACs
recentMACs := macTable.Find(func(e *frame.MACEntry) bool {
return time.Since(e.LastSeen) < time.Minute
})

To handle additional protocols:

// Add new EtherType constant
const (
EtherTypeMyProtocol frame.EtherType = 0x88B5 // Example
)
// Check for custom protocol in handler
if ethFrame.Header.EtherType == EtherTypeMyProtocol {
handleMyProtocol(ethFrame.Payload)
}
ErrorCauseSolution
ErrFrameTooShortFrame < 14 bytesCheck TAP device reads
ErrFrameTooLongFrame > 1522 bytesCheck MTU settings
ErrInvalidMACMAC not 6 bytesValidate input addresses
ErrPayloadTooLargePayload > 1500 bytesFragment or reduce MTU
ErrARPTooShortARP packet < 28 bytesCheck frame parsing
ErrARPInvalidTypeNon-Ethernet/IPv4 ARPOnly Ethernet/IPv4 supported
frame, err := frame.ParseEthernetFrame(data)
if err != nil {
switch {
case errors.Is(err, frame.ErrFrameTooShort):
// Log and skip malformed frame
slog.Debug("frame too short", "len", len(data))
case errors.Is(err, frame.ErrFrameTooLong):
// Possible MTU issue
slog.Warn("frame too long", "len", len(data))
default:
slog.Error("frame parse error", "error", err)
}
return
}
  • Frames use Raw field to preserve original bytes for forwarding
  • MAC table uses pre-allocated maps
  • Entry expiration prevents unbounded growth
  1. Minimize copying: Use frame.Raw for forwarding instead of rebuilding
  2. Batch operations: Process multiple frames before flushing
  3. Tune MAC table expiry: Balance memory vs. lookup misses

Typical performance characteristics:

OperationApproximate Time
Parse Ethernet frame~50 ns
Build Ethernet frame~100 ns
MAC table lookup~20 ns
MAC table learn~50 ns
ARP packet parse~80 ns

Enable debug logging to trace frame handling:

slog.Debug("processing frame",
"src_mac", ethFrame.Header.SrcMAC,
"dst_mac", ethFrame.Header.DstMAC,
"ether_type", ethFrame.Header.EtherType,
"payload_len", len(ethFrame.Payload),
)
// Get all entries for debugging
entries := macTable.All()
for _, e := range entries {
slog.Info("mac entry",
"mac", e.MAC,
"peer_id", e.PeerID,
"ip", e.VirtualIP,
"age", time.Since(e.LearnedAt),
)
}
SymptomPossible CauseDebug Steps
Frames not reaching peersMAC not learnedCheck macTable.Lookup()
ARP not resolvingARP interceptor not handlingVerify localIP matches
High broadcast trafficMAC table missing entriesCheck expiry settings
Duplicate framesLoop in forwardingVerify source MAC learning
MethodReturnsDescription
IsBroadcast()boolCheck if destination is broadcast
IsMulticast()boolCheck if destination is multicast
IsUnicast()boolCheck if destination is unicast
IsIPv4()boolCheck if payload is IPv4
IsIPv6()boolCheck if payload is IPv6
IsARP()boolCheck if payload is ARP
IsIP()boolCheck if payload is IP (v4 or v6)
Clone()*EthernetFrameDeep copy the frame
MarshalBinary()([]byte, error)Serialize to bytes
ExtractIPAddresses()(src, dst IP, error)Get IP addresses from payload
String()stringHuman-readable representation
MethodReturnsDescription
Learn(mac, peerID)voidLearn/update MAC entry
LearnWithIP(mac, peerID, ip)voidLearn with IP association
LearnStatic(mac, peerID, ip)voidAdd non-expiring entry
Lookup(mac)(peerID, bool)Get peer ID for MAC
LookupEntry(mac)(*MACEntry, bool)Get full entry
LookupByIP(ip)(MAC, bool)Reverse lookup by IP
GetPeerMACs(peerID)[]MACGet all MACs for peer
Remove(mac)voidRemove specific entry
RemovePeer(peerID)voidRemove all peer entries
Expire()intRemove stale entries
Clear()voidRemove all entries
Count()intNumber of entries
All()[]*MACEntryAll current entries
MethodReturnsDescription
HandleFrame(data)[]byteProcess frame, return response
HandlePacket(data)([]byte, error)Process ARP packet only
LocalMAC()HardwareAddrGet local MAC address
LocalIP()netip.AddrGet local IP address