Refactor AARP

This commit is contained in:
Josh Deprez 2024-04-06 17:46:00 +11:00
parent 345bee4979
commit dd68ef97b6
Signed by: josh
SSH key fingerprint: SHA256:zZji7w1Ilh2RuUpbQcqkLPrqmRwpiCSycbF2EfKm6Kw
4 changed files with 260 additions and 132 deletions

226
aarp.go Normal file
View file

@ -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
}

46
amt.go
View file

@ -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
}

View file

@ -19,6 +19,7 @@ package main
import ( import (
"os" "os"
"github.com/sfiera/multitalk/pkg/ddp"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -35,10 +36,10 @@ type config struct {
// Required for routing a local EtherTalk network. // Required for routing a local EtherTalk network.
EtherTalk struct { EtherTalk struct {
Device string `yaml:"device"` Device string `yaml:"device"`
ZoneName string `yaml:"zone_name"` ZoneName string `yaml:"zone_name"`
NetStart uint16 `yaml:"net_start"` NetStart ddp.Network `yaml:"net_start"`
NetEnd uint16 `yaml:"net_end"` NetEnd ddp.Network `yaml:"net_end"`
} `yaml:"ethertalk"` } `yaml:"ethertalk"`
// LocalTalk struct { // LocalTalk struct {

111
main.go
View file

@ -31,7 +31,6 @@ import (
"gitea.drjosh.dev/josh/jrouter/atalk" "gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/aurp" "gitea.drjosh.dev/josh/jrouter/aurp"
"github.com/sfiera/multitalk/pkg/aarp"
"github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet" "github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk" "github.com/sfiera/multitalk/pkg/ethertalk"
@ -138,101 +137,49 @@ func main() {
goHandler(peer) goHandler(peer)
} }
// AppleTalk packet loop // AppleTalk packet loops
var amt AMT 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() { 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 { for {
rawPkt, _, err := handle.ReadPacketData() rawPkt, _, err := pcapHandle.ReadPacketData()
if err != nil { if err != nil {
log.Fatalf("Couldn't read AppleTalk / AARP packet data: %v", err) log.Fatalf("Couldn't read AppleTalk / AARP packet data: %v", err)
} }
var ethFrame ethertalk.Packet ethFrame := new(ethertalk.Packet)
if err := ethertalk.Unmarshal(rawPkt, &ethFrame); err != nil { if err := ethertalk.Unmarshal(rawPkt, ethFrame); err != nil {
log.Printf("Couldn't unmarshal EtherTalk frame: %v", err) log.Printf("Couldn't unmarshal EtherTalk frame: %v", err)
continue continue
} }
if ethFrame.Src == localMAC { // Ignore if sent by me
if ethFrame.Src == myHWAddr {
continue continue
} }
switch ethFrame.SNAPProto { switch ethFrame.SNAPProto {
case ethertalk.AARPProto: case ethertalk.AARPProto:
var aapkt aarp.Packet aarpCh <- ethFrame
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
}
case ethertalk.AppleTalkProto: case ethertalk.AppleTalkProto:
var ddpkt ddp.ExtPacket var ddpkt ddp.ExtPacket
@ -246,7 +193,7 @@ func main() {
ddpkt.Proto, len(ddpkt.Data)) ddpkt.Proto, len(ddpkt.Data))
// Glean address info for AMT // Glean address info for AMT
srcAddr := ddp.Addr{Network: ddpkt.SrcNet, Node: ddpkt.SrcNode} 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) log.Printf("DDP: Gleaned that %v -> %v", srcAddr, ethFrame.Src)
default: default: