Refactor AARP
This commit is contained in:
parent
345bee4979
commit
dd68ef97b6
4 changed files with 260 additions and 132 deletions
226
aarp.go
Normal file
226
aarp.go
Normal 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
46
amt.go
|
@ -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
|
|
||||||
}
|
|
|
@ -19,6 +19,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/sfiera/multitalk/pkg/ddp"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,8 +38,8 @@ type config struct {
|
||||||
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 {
|
||||||
|
|
91
main.go
91
main.go
|
@ -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
|
|
||||||
go func() {
|
|
||||||
iface, err := net.InterfaceByName(cfg.EtherTalk.Device)
|
iface, err := net.InterfaceByName(cfg.EtherTalk.Device)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Couldn't find interface named %q: %v", cfg.EtherTalk.Device, err)
|
log.Fatalf("Couldn't find interface named %q: %v", cfg.EtherTalk.Device, err)
|
||||||
}
|
}
|
||||||
localMAC := ethernet.Addr(iface.HardwareAddr)
|
myHWAddr := ethernet.Addr(iface.HardwareAddr)
|
||||||
|
|
||||||
handle, err := atalk.StartPcap(cfg.EtherTalk.Device)
|
pcapHandle, err := atalk.StartPcap(cfg.EtherTalk.Device)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Couldn't open network device for AppleTalk: %v", err)
|
log.Fatalf("Couldn't open network device for AppleTalk: %v", err)
|
||||||
}
|
}
|
||||||
defer handle.Close()
|
defer pcapHandle.Close()
|
||||||
|
|
||||||
// AARP probe for our preferred address (first network.1)
|
aarpMachine := &AARPMachine{
|
||||||
localDDPAddr := ddp.Addr{
|
AMT: new(AMT),
|
||||||
Network: ddp.Network(cfg.EtherTalk.NetStart),
|
cfg: cfg,
|
||||||
Node: 1,
|
pcapHandle: pcapHandle,
|
||||||
}
|
myHWAddr: myHWAddr,
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
aarpCh := make(chan *ethertalk.Packet, 1024)
|
||||||
|
go aarpMachine.Run(ctx, aarpCh)
|
||||||
|
|
||||||
|
go func() {
|
||||||
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, ðFrame); 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:
|
||||||
|
|
Loading…
Reference in a new issue