jrouter/aarp.go

323 lines
8.2 KiB
Go

package main
import (
"context"
"log"
"math/rand/v2"
"sync"
"time"
"github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/aarp"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
)
const (
// TODO: verify parameters
maxAMTEntryAge = 30 * time.Second
aarpRequestRetransmit = 1 * time.Second
aarpRequestTimeout = 10 * time.Second
)
// AARPMachine maintains both an Address Mapping Table and handles AARP packets
// (sending and receiving requests, responses, and probes). This process assumes
// a particular network range rather than using the startup range, since this
// program is a seed router.
type AARPMachine struct {
*addressMappingTable
cfg *config
pcapHandle *pcap.Handle
// The Run goroutine is responsible for all writes to myAddr.Proto and
// probes, so this mutex is not used to enforce a single writer, only
// consistent reads
mu sync.RWMutex
myAddr aarp.AddrPair
probes int
assignedCh chan struct{}
}
// NewAARPMachine creates a new AARPMachine.
func NewAARPMachine(cfg *config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr) *AARPMachine {
return &AARPMachine{
addressMappingTable: new(addressMappingTable),
cfg: cfg,
pcapHandle: pcapHandle,
myAddr: aarp.AddrPair{
Hardware: myHWAddr,
},
assignedCh: make(chan struct{}),
}
}
// Address returns the address of this node, and reports if the address is valid
// (i.e. not tentative).
func (a *AARPMachine) Address() (aarp.AddrPair, bool) {
a.mu.RLock()
defer a.mu.RUnlock()
return a.myAddr, a.assigned()
}
// Assigned returns a channel that is closed when the local address is valid.
func (a *AARPMachine) Assigned() <-chan struct{} {
return a.assignedCh
}
// Run executes the machine.
func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Packet) error {
// Initialise our DDP address with a preferred address (first network.1)
a.mu.Lock()
a.probes = 0
a.myAddr.Proto = ddp.Addr{
Network: ddp.Network(a.cfg.EtherTalk.NetStart),
Node: 1,
}
a.mu.Unlock()
ticker := time.NewTicker(200 * time.Millisecond) // 200ms is the AARP probe retransmit
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if a.assigned() {
close(a.assignedCh)
// No need to keep the ticker running if assigned
ticker.Stop()
continue
}
a.mu.Lock()
a.probes++
a.mu.Unlock()
if err := a.probe(); err != nil {
log.Printf("Couldn't broadcast a Probe: %v", err)
}
case ethFrame, ok := <-incomingCh:
if !ok {
incomingCh = nil
}
var aapkt aarp.Packet
if err := aarp.Unmarshal(ethFrame.Payload, &aapkt); err != nil {
log.Printf("Couldn't unmarshal AARP packet: %v", err)
continue
}
switch aapkt.Opcode {
case aarp.RequestOp:
log.Printf("AARP: Who has %v? Tell %v", aapkt.Dst.Proto, aapkt.Src.Proto)
// Glean that aapkt.Src.Proto -> aapkt.Src.Hardware
a.addressMappingTable.Learn(aapkt.Src.Proto, aapkt.Src.Hardware)
log.Printf("AARP: Gleaned that %v -> %v", aapkt.Src.Proto, aapkt.Src.Hardware)
if !(aapkt.Dst.Proto == a.myAddr.Proto && a.assigned()) {
continue
}
// Hey that's me! Let them know!
if err := a.heyThatsMe(aapkt.Src); err != nil {
log.Printf("AARP: Couldn't respond to Request: %v", err)
continue
}
case aarp.ResponseOp:
log.Printf("AARP: %v is at %v", aapkt.Dst.Proto, aapkt.Dst.Hardware)
a.addressMappingTable.Learn(aapkt.Dst.Proto, aapkt.Dst.Hardware)
if aapkt.Dst.Proto != a.myAddr.Proto {
continue
}
if !a.assigned() {
a.reroll()
}
case aarp.ProbeOp:
log.Printf("AARP: %v probing to see if %v is available", aapkt.Src.Hardware, aapkt.Src.Proto)
// AMT should not be updated, because the address is tentative
if aapkt.Dst.Proto != a.myAddr.Proto {
continue
}
if !a.assigned() {
// Another node is probing for the same address! Unlucky
a.reroll()
continue
}
if err := a.heyThatsMe(aapkt.Src); err != nil {
log.Printf("AARP: Couldn't respond to Probe: %v", err)
continue
}
}
}
}
}
// Resolve resolves an AppleTalk node address to an Ethernet address.
// If the address is in the cache (AMT) and is still valid, that is used.
// Otherwise, the address is resolved using AARP.
func (a *AARPMachine) Resolve(ctx context.Context, ddpAddr ddp.Addr) (ethernet.Addr, error) {
result, waitCh := a.lookupOrWait(ddpAddr)
if waitCh == nil {
return result, nil
}
if err := a.request(ddpAddr); err != nil {
return ethernet.Addr{}, err
}
ticker := time.NewTicker(aarpRequestRetransmit)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(ctx, aarpRequestTimeout)
defer cancel()
for {
select {
case <-ctx.Done():
return ethernet.Addr{}, ctx.Err()
case <-waitCh:
result, waitCh = a.lookupOrWait(ddpAddr)
if waitCh == nil {
return result, nil
}
case <-ticker.C:
if err := a.request(ddpAddr); err != nil {
return ethernet.Addr{}, err
}
}
}
}
func (a *AARPMachine) assigned() bool { return a.probes >= 10 }
// Re-roll a local address
func (a *AARPMachine) reroll() {
a.mu.Lock()
defer a.mu.Unlock()
if a.cfg.EtherTalk.NetStart != a.cfg.EtherTalk.NetEnd {
// Pick a new network number at random
a.myAddr.Proto.Network = rand.N(
a.cfg.EtherTalk.NetEnd-a.cfg.EtherTalk.NetStart+1,
) + a.cfg.EtherTalk.NetStart
}
// Can't use: 0x00, 0xff, 0xfe, or the existing node number
newNode := rand.N[ddp.Node](0xfd) + 1
for newNode != a.myAddr.Proto.Node {
newNode = rand.N[ddp.Node](0xfd) + 1
}
a.myAddr.Proto.Node = newNode
a.probes = 0
}
// Send an AARP response
func (a *AARPMachine) heyThatsMe(targ aarp.AddrPair) error {
respFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Response(targ, a.myAddr))
if err != nil {
return err
}
// Instead of broadcasting the reply, send it to the target specifically
respFrame.Dst = targ.Hardware
respFrameRaw, err := ethertalk.Marshal(*respFrame)
if err != nil {
return err
}
return a.pcapHandle.WritePacketData(respFrameRaw)
}
// Broadcast an AARP Probe
func (a *AARPMachine) probe() error {
probeFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Probe(a.myAddr.Hardware, a.myAddr.Proto))
if err != nil {
return err
}
probeFrameRaw, err := ethertalk.Marshal(*probeFrame)
if err != nil {
return err
}
return a.pcapHandle.WritePacketData(probeFrameRaw)
}
// Broadcast an AARP Request
func (a *AARPMachine) request(ddpAddr ddp.Addr) error {
reqFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Request(a.myAddr, ddpAddr))
if err != nil {
return err
}
reqFrameRaw, err := ethertalk.Marshal(*reqFrame)
if err != nil {
return err
}
return a.pcapHandle.WritePacketData(reqFrameRaw)
}
type amtEntry struct {
hwAddr ethernet.Addr
last time.Time
updated chan struct{}
}
// addressMappingTable implements a concurrent-safe Address Mapping Table for
// AppleTalk (DDP) addresses to Ethernet hardware addresses.
type addressMappingTable struct {
mu sync.Mutex
table map[ddp.Addr]*amtEntry
}
// Learn adds or updates an AMT entry.
func (t *addressMappingTable) Learn(ddpAddr ddp.Addr, hwAddr ethernet.Addr) {
t.mu.Lock()
defer t.mu.Unlock()
if t.table == nil {
t.table = make(map[ddp.Addr]*amtEntry)
}
oldEnt := t.table[ddpAddr]
if oldEnt == nil {
t.table[ddpAddr] = &amtEntry{
hwAddr: hwAddr,
last: time.Now(),
updated: make(chan struct{}),
}
return
}
if oldEnt.hwAddr == hwAddr && time.Since(oldEnt.last) < maxAMTEntryAge {
oldEnt.last = time.Now()
return
}
oldEnt.hwAddr = hwAddr
oldEnt.last = time.Now()
close(oldEnt.updated)
oldEnt.updated = make(chan struct{})
}
// lookupOrWait returns either the valid cached Ethernet address for the given
// DDP address, or a channel that is closed when the entry is updated.
func (t *addressMappingTable) lookupOrWait(ddpAddr ddp.Addr) (ethernet.Addr, <-chan struct{}) {
t.mu.Lock()
defer t.mu.Unlock()
if t.table == nil {
t.table = make(map[ddp.Addr]*amtEntry)
}
ent, ok := t.table[ddpAddr]
if ok && time.Since(ent.last) < maxAMTEntryAge {
return ent.hwAddr, nil
}
ch := make(chan struct{})
t.table[ddpAddr] = &amtEntry{
updated: ch,
}
return ethernet.Addr{}, ch
}