From dd68ef97b67efbb4256d69426fd0468d689aa062 Mon Sep 17 00:00:00 2001 From: Josh Deprez Date: Sat, 6 Apr 2024 17:46:00 +1100 Subject: [PATCH] Refactor AARP --- aarp.go | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ amt.go | 46 ----------- config.go | 9 ++- main.go | 111 +++++++-------------------- 4 files changed, 260 insertions(+), 132 deletions(-) create mode 100644 aarp.go delete mode 100644 amt.go diff --git a/aarp.go b/aarp.go new file mode 100644 index 0000000..7d5d3c6 --- /dev/null +++ b/aarp.go @@ -0,0 +1,226 @@ +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 +} diff --git a/amt.go b/amt.go deleted file mode 100644 index b9d545c..0000000 --- a/amt.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "sync" - "time" - - "github.com/sfiera/multitalk/pkg/ddp" - "github.com/sfiera/multitalk/pkg/ethernet" -) - -// TODO: verify this parameter -const maxAMTEntryAge = 30 * time.Second - -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 -} diff --git a/config.go b/config.go index 6a5ab93..1b53bce 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,7 @@ package main import ( "os" + "github.com/sfiera/multitalk/pkg/ddp" "gopkg.in/yaml.v3" ) @@ -35,10 +36,10 @@ type config struct { // Required for routing a local EtherTalk network. EtherTalk struct { - Device string `yaml:"device"` - ZoneName string `yaml:"zone_name"` - NetStart uint16 `yaml:"net_start"` - NetEnd uint16 `yaml:"net_end"` + Device string `yaml:"device"` + ZoneName string `yaml:"zone_name"` + NetStart ddp.Network `yaml:"net_start"` + NetEnd ddp.Network `yaml:"net_end"` } `yaml:"ethertalk"` // LocalTalk struct { diff --git a/main.go b/main.go index 36d5adf..bd70bcf 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,6 @@ import ( "gitea.drjosh.dev/josh/jrouter/atalk" "gitea.drjosh.dev/josh/jrouter/aurp" - "github.com/sfiera/multitalk/pkg/aarp" "github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ethernet" "github.com/sfiera/multitalk/pkg/ethertalk" @@ -138,101 +137,49 @@ func main() { goHandler(peer) } - // AppleTalk packet loop - var amt AMT + // AppleTalk packet loops + iface, err := net.InterfaceByName(cfg.EtherTalk.Device) + if err != nil { + log.Fatalf("Couldn't find interface named %q: %v", cfg.EtherTalk.Device, err) + } + myHWAddr := ethernet.Addr(iface.HardwareAddr) + + pcapHandle, err := atalk.StartPcap(cfg.EtherTalk.Device) + if err != nil { + log.Fatalf("Couldn't open network device for AppleTalk: %v", err) + } + defer pcapHandle.Close() + + aarpMachine := &AARPMachine{ + AMT: new(AMT), + cfg: cfg, + pcapHandle: pcapHandle, + myHWAddr: myHWAddr, + } + aarpCh := make(chan *ethertalk.Packet, 1024) + go aarpMachine.Run(ctx, aarpCh) + go func() { - iface, err := net.InterfaceByName(cfg.EtherTalk.Device) - if err != nil { - log.Fatalf("Couldn't find interface named %q: %v", cfg.EtherTalk.Device, err) - } - localMAC := ethernet.Addr(iface.HardwareAddr) - - handle, err := atalk.StartPcap(cfg.EtherTalk.Device) - if err != nil { - log.Fatalf("Couldn't open network device for AppleTalk: %v", err) - } - defer handle.Close() - - // AARP probe for our preferred address (first network.1) - localDDPAddr := ddp.Addr{ - Network: ddp.Network(cfg.EtherTalk.NetStart), - Node: 1, - } - - probeFrame, err := ethertalk.AARP(localMAC, aarp.Probe(localMAC, localDDPAddr)) - if err != nil { - log.Fatalf("Couldn't construct AARP Probe: %v", err) - } - probeFrameRaw, err := ethertalk.Marshal(*probeFrame) - if err != nil { - log.Fatalf("Couldn't marshal AARP Probe: %v", err) - } - if err := handle.WritePacketData(probeFrameRaw); err != nil { - log.Fatalf("Couldn't write packet data: %v", err) - } - for { - rawPkt, _, err := handle.ReadPacketData() + rawPkt, _, err := pcapHandle.ReadPacketData() if err != nil { log.Fatalf("Couldn't read AppleTalk / AARP packet data: %v", err) } - var ethFrame ethertalk.Packet - if err := ethertalk.Unmarshal(rawPkt, ðFrame); err != nil { + ethFrame := new(ethertalk.Packet) + if err := ethertalk.Unmarshal(rawPkt, ethFrame); err != nil { log.Printf("Couldn't unmarshal EtherTalk frame: %v", err) continue } - if ethFrame.Src == localMAC { + // Ignore if sent by me + if ethFrame.Src == myHWAddr { continue } switch ethFrame.SNAPProto { case ethertalk.AARPProto: - 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 - 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 != localDDPAddr { - continue - } - // Respond! - respFrame, err := ethertalk.AARP(localMAC, aarp.Response(aapkt.Src, aarp.AddrPair{ - Proto: localDDPAddr, - Hardware: localMAC, - })) - if err != nil { - log.Printf("Couldn't construct AARP Response: %v", err) - continue - } - respFrame.Dst = ethFrame.Src - respFrameRaw, err := ethertalk.Marshal(*respFrame) - if err != nil { - log.Printf("Couldn't marshal AARP Response: %v", err) - continue - } - if err := handle.WritePacketData(respFrameRaw); err != nil { - log.Printf("Couldn't write packet data: %v", err) - continue - } - - case aarp.ResponseOp: - log.Printf("AARP: %v is at %v", aapkt.Dst.Proto, aapkt.Dst.Hardware) - amt.Learn(aapkt.Dst.Proto, aapkt.Dst.Hardware) - - 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 - } + aarpCh <- ethFrame case ethertalk.AppleTalkProto: var ddpkt ddp.ExtPacket @@ -246,7 +193,7 @@ func main() { ddpkt.Proto, len(ddpkt.Data)) // Glean address info for AMT srcAddr := ddp.Addr{Network: ddpkt.SrcNet, Node: ddpkt.SrcNode} - amt.Learn(srcAddr, ethFrame.Src) + aarpMachine.Learn(srcAddr, ethFrame.Src) log.Printf("DDP: Gleaned that %v -> %v", srcAddr, ethFrame.Src) default: