227 lines
5.4 KiB
Go
227 lines
5.4 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"
|
||
|
)
|
||
|
|
||
|
// TODO: verify this parameter
|
||
|
const maxAMTEntryAge = 30 * 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 {
|
||
|
*AMT
|
||
|
|
||
|
cfg *config
|
||
|
pcapHandle *pcap.Handle
|
||
|
|
||
|
state aarpState
|
||
|
probes int
|
||
|
|
||
|
myHWAddr ethernet.Addr
|
||
|
myDDPAddr ddp.Addr
|
||
|
}
|
||
|
|
||
|
type aarpState int
|
||
|
|
||
|
const (
|
||
|
aarpStateProbing aarpState = iota
|
||
|
aarpStateAssigned
|
||
|
)
|
||
|
|
||
|
func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Packet) error {
|
||
|
ticker := time.NewTicker(200 * time.Millisecond) // 200ms is the AARP probe retransmit
|
||
|
defer ticker.Stop()
|
||
|
|
||
|
a.state = aarpStateProbing
|
||
|
a.probes = 0
|
||
|
|
||
|
// Initialise our DDP address with a preferred address (first network.1)
|
||
|
a.myDDPAddr = ddp.Addr{
|
||
|
Network: ddp.Network(a.cfg.EtherTalk.NetStart),
|
||
|
Node: 1,
|
||
|
}
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
return ctx.Err()
|
||
|
|
||
|
case <-ticker.C:
|
||
|
switch a.state {
|
||
|
case aarpStateAssigned:
|
||
|
// No need to keep the ticker running if assigned
|
||
|
ticker.Stop()
|
||
|
|
||
|
case aarpStateProbing:
|
||
|
if a.probes >= 10 {
|
||
|
a.state = aarpStateAssigned
|
||
|
continue
|
||
|
}
|
||
|
a.probes++
|
||
|
if err := a.probe(); err != nil {
|
||
|
log.Printf("Couldn't broadcast a Probe: %v", err)
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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.AMT.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.myDDPAddr {
|
||
|
continue
|
||
|
}
|
||
|
if a.state != aarpStateAssigned {
|
||
|
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.AMT.Learn(aapkt.Dst.Proto, aapkt.Dst.Hardware)
|
||
|
|
||
|
if aapkt.Dst.Proto != a.myDDPAddr {
|
||
|
continue
|
||
|
}
|
||
|
if a.state == aarpStateProbing {
|
||
|
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.myDDPAddr {
|
||
|
continue
|
||
|
}
|
||
|
switch a.state {
|
||
|
case aarpStateProbing:
|
||
|
// Another node is probing for the same address! Unlucky
|
||
|
a.reroll()
|
||
|
|
||
|
case aarpStateAssigned:
|
||
|
if err := a.heyThatsMe(aapkt.Src); err != nil {
|
||
|
log.Printf("AARP: Couldn't respond to Probe: %v", err)
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Re-roll a local address
|
||
|
func (a *AARPMachine) reroll() {
|
||
|
if a.cfg.EtherTalk.NetStart != a.cfg.EtherTalk.NetEnd {
|
||
|
// Pick a new network number at random
|
||
|
a.myDDPAddr.Network = rand.N[ddp.Network](
|
||
|
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.myDDPAddr.Node {
|
||
|
newNode = rand.N[ddp.Node](0xfd) + 1
|
||
|
}
|
||
|
a.myDDPAddr.Node = newNode
|
||
|
a.probes = 0
|
||
|
}
|
||
|
|
||
|
// Send an AARP response
|
||
|
func (a *AARPMachine) heyThatsMe(targ aarp.AddrPair) error {
|
||
|
respFrame, err := ethertalk.AARP(a.myHWAddr, aarp.Response(targ, aarp.AddrPair{
|
||
|
Proto: a.myDDPAddr,
|
||
|
Hardware: a.myHWAddr,
|
||
|
}))
|
||
|
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.myHWAddr, aarp.Probe(a.myHWAddr, a.myDDPAddr))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
probeFrameRaw, err := ethertalk.Marshal(*probeFrame)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return a.pcapHandle.WritePacketData(probeFrameRaw)
|
||
|
}
|
||
|
|
||
|
type amtEntry struct {
|
||
|
hwAddr ethernet.Addr
|
||
|
last time.Time
|
||
|
}
|
||
|
|
||
|
// AMT implements a concurrent-safe Address Mapping Table for AppleTalk (DDP)
|
||
|
// addresses to Ethernet hardware addresses.
|
||
|
type AMT struct {
|
||
|
mu sync.RWMutex
|
||
|
table map[ddp.Addr]amtEntry
|
||
|
}
|
||
|
|
||
|
// Learn adds or updates an AMT entry.
|
||
|
func (t *AMT) 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)
|
||
|
}
|
||
|
t.table[ddpAddr] = amtEntry{
|
||
|
hwAddr: hwAddr,
|
||
|
last: time.Now(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Lookup searches for a non-expired entry in the table only. It does not send
|
||
|
// any packets.
|
||
|
func (t *AMT) Lookup(ddpAddr ddp.Addr) (ethernet.Addr, bool) {
|
||
|
t.mu.RLock()
|
||
|
defer t.mu.RUnlock()
|
||
|
ent, ok := t.table[ddpAddr]
|
||
|
return ent.hwAddr, ok && time.Since(ent.last) < maxAMTEntryAge
|
||
|
}
|