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 (
|
||||
"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 {
|
||||
|
|
111
main.go
111
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:
|
||||
|
|
Loading…
Reference in a new issue