jrouter/aarp.go

381 lines
9.8 KiB
Go
Raw Normal View History

2024-04-08 09:21:35 +10:00
/*
Copyright 2024 Josh Deprez
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
2024-04-06 17:46:00 +11:00
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"
)
2024-04-06 18:20:59 +11:00
const (
// TODO: verify parameters
maxAMTEntryAge = 30 * time.Second
aarpRequestRetransmit = 1 * time.Second
aarpRequestTimeout = 10 * time.Second
)
2024-04-06 17:46:00 +11:00
// 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 {
2024-04-07 12:09:58 +10:00
*addressMappingTable
2024-04-06 17:46:00 +11:00
cfg *config
pcapHandle *pcap.Handle
2024-04-07 12:09:58 +10:00
// The Run goroutine is responsible for all writes to myAddr.Proto and
// probes, so this mutex is not used to enforce a single writer, only
// consistent reads
2024-04-07 17:01:26 +10:00
mu sync.RWMutex
myAddr aarp.AddrPair
probes int
2024-04-12 15:20:04 +10:00
assigned bool
2024-04-07 17:01:26 +10:00
assignedCh chan struct{}
2024-04-06 17:46:00 +11:00
}
2024-04-07 12:09:58 +10:00
// NewAARPMachine creates a new AARPMachine.
2024-04-06 20:11:15 +11:00
func NewAARPMachine(cfg *config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr) *AARPMachine {
return &AARPMachine{
2024-04-07 12:09:58 +10:00
addressMappingTable: new(addressMappingTable),
cfg: cfg,
pcapHandle: pcapHandle,
2024-04-06 20:11:15 +11:00
myAddr: aarp.AddrPair{
Hardware: myHWAddr,
},
2024-04-07 17:01:26 +10:00
assignedCh: make(chan struct{}),
2024-04-06 20:11:15 +11:00
}
}
2024-04-06 17:46:00 +11:00
2024-04-07 12:09:58 +10:00
// Address returns the address of this node, and reports if the address is valid
// (i.e. not tentative).
func (a *AARPMachine) Address() (aarp.AddrPair, bool) {
a.mu.RLock()
defer a.mu.RUnlock()
2024-04-12 15:20:04 +10:00
return a.myAddr, a.assigned
2024-04-07 12:09:58 +10:00
}
2024-04-06 17:46:00 +11:00
2024-04-07 17:01:26 +10:00
// Assigned returns a channel that is closed when the local address is valid.
func (a *AARPMachine) Assigned() <-chan struct{} {
return a.assignedCh
}
2024-04-07 12:09:58 +10:00
// Run executes the machine.
func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Packet) error {
2024-04-06 17:46:00 +11:00
// Initialise our DDP address with a preferred address (first network.1)
2024-04-07 12:09:58 +10:00
a.mu.Lock()
a.probes = 0
2024-04-06 18:20:59 +11:00
a.myAddr.Proto = ddp.Addr{
2024-04-06 17:46:00 +11:00
Network: ddp.Network(a.cfg.EtherTalk.NetStart),
Node: 1,
}
2024-04-07 12:09:58 +10:00
a.mu.Unlock()
ticker := time.NewTicker(200 * time.Millisecond) // 200ms is the AARP probe retransmit
defer ticker.Stop()
2024-04-06 17:46:00 +11:00
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
2024-04-12 15:20:04 +10:00
if a.probes >= 10 {
a.mu.Lock()
a.assigned = true
a.mu.Unlock()
2024-04-07 17:01:26 +10:00
close(a.assignedCh)
2024-04-06 17:46:00 +11:00
ticker.Stop()
2024-04-07 12:09:58 +10:00
continue
}
2024-04-06 17:46:00 +11:00
2024-04-07 12:09:58 +10:00
a.mu.Lock()
a.probes++
a.mu.Unlock()
if err := a.probe(); err != nil {
log.Printf("Couldn't broadcast a Probe: %v", err)
2024-04-06 17:46:00 +11:00
}
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:
2024-04-13 18:25:14 +10:00
log.Printf("AARP: Who has %d.%d? Tell %d.%d",
aapkt.Dst.Proto.Network, aapkt.Dst.Proto.Node,
aapkt.Src.Proto.Network, aapkt.Src.Proto.Node,
)
2024-04-06 17:46:00 +11:00
// Glean that aapkt.Src.Proto -> aapkt.Src.Hardware
2024-04-07 12:09:58 +10:00
a.addressMappingTable.Learn(aapkt.Src.Proto, aapkt.Src.Hardware)
2024-04-14 13:32:49 +10:00
// log.Printf("AARP: Gleaned that %d.%d -> %v", aapkt.Src.Proto.Network, aapkt.Src.Proto.Node, aapkt.Src.Hardware)
2024-04-06 17:46:00 +11:00
2024-04-13 17:48:57 +10:00
if aapkt.Dst.Proto != a.myAddr.Proto {
log.Printf("AARP: not replying to request for %d.%d (not my address)", aapkt.Dst.Proto.Network, aapkt.Dst.Proto.Node)
continue
}
if !a.assigned {
log.Printf("AARP: not replying to request for %d.%d (address still tentative)", aapkt.Dst.Proto.Network, aapkt.Dst.Proto.Node)
2024-04-06 17:46:00 +11:00
continue
}
2024-04-07 12:09:58 +10:00
2024-04-06 17:46:00 +11:00
// 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:
2024-04-13 18:25:14 +10:00
log.Printf("AARP: %d.%d is at %v",
aapkt.Dst.Proto.Network, aapkt.Dst.Proto.Node, aapkt.Dst.Hardware,
)
2024-04-07 12:09:58 +10:00
a.addressMappingTable.Learn(aapkt.Dst.Proto, aapkt.Dst.Hardware)
2024-04-06 17:46:00 +11:00
2024-04-06 18:20:59 +11:00
if aapkt.Dst.Proto != a.myAddr.Proto {
2024-04-06 17:46:00 +11:00
continue
}
2024-04-12 15:20:04 +10:00
if !a.assigned {
2024-04-06 17:46:00 +11:00
a.reroll()
}
case aarp.ProbeOp:
2024-04-13 18:25:14 +10:00
log.Printf("AARP: %v probing to see if %d.%d is available",
aapkt.Src.Hardware, aapkt.Src.Proto.Network, aapkt.Src.Proto.Node,
)
2024-04-06 17:46:00 +11:00
// AMT should not be updated, because the address is tentative
2024-04-06 18:20:59 +11:00
if aapkt.Dst.Proto != a.myAddr.Proto {
2024-04-06 17:46:00 +11:00
continue
}
2024-04-12 15:20:04 +10:00
if !a.assigned {
2024-04-06 17:46:00 +11:00
// Another node is probing for the same address! Unlucky
a.reroll()
2024-04-07 12:09:58 +10:00
continue
}
2024-04-06 17:46:00 +11:00
2024-04-07 12:09:58 +10:00
if err := a.heyThatsMe(aapkt.Src); err != nil {
log.Printf("AARP: Couldn't respond to Probe: %v", err)
continue
2024-04-06 17:46:00 +11:00
}
}
}
}
}
2024-04-06 18:20:59 +11:00
// Resolve resolves an AppleTalk node address to an Ethernet address.
// If the address is in the cache (AMT) and is still valid, that is used.
// Otherwise, the address is resolved using AARP.
func (a *AARPMachine) Resolve(ctx context.Context, ddpAddr ddp.Addr) (ethernet.Addr, error) {
2024-04-12 15:20:04 +10:00
result, waitCh, winner := a.lookupOrWait(ddpAddr)
2024-04-07 12:09:58 +10:00
if waitCh == nil {
2024-04-06 18:20:59 +11:00
return result, nil
}
2024-04-13 15:18:33 +10:00
if winner {
if err := a.request(ddpAddr); err != nil {
return ethernet.Addr{}, err
2024-04-12 15:20:04 +10:00
}
}
2024-04-06 18:20:59 +11:00
ticker := time.NewTicker(aarpRequestRetransmit)
defer ticker.Stop()
2024-04-13 15:18:33 +10:00
ctx, cancel := context.WithTimeout(ctx, aarpRequestTimeout)
defer cancel()
2024-04-06 18:20:59 +11:00
for {
select {
case <-ctx.Done():
2024-04-13 15:18:33 +10:00
a.requestingStopped(ddpAddr)
2024-04-06 18:20:59 +11:00
return ethernet.Addr{}, ctx.Err()
2024-04-07 12:09:58 +10:00
case <-waitCh:
2024-04-13 15:18:33 +10:00
result, waitCh, winner = a.lookupOrWait(ddpAddr)
2024-04-07 12:09:58 +10:00
if waitCh == nil {
return result, nil
}
2024-04-06 18:20:59 +11:00
case <-ticker.C:
2024-04-13 15:18:33 +10:00
if !winner {
continue
}
2024-04-06 18:20:59 +11:00
if err := a.request(ddpAddr); err != nil {
return ethernet.Addr{}, err
}
}
}
}
2024-04-06 17:46:00 +11:00
// Re-roll a local address
func (a *AARPMachine) reroll() {
2024-04-07 12:09:58 +10:00
a.mu.Lock()
defer a.mu.Unlock()
2024-04-06 17:46:00 +11:00
if a.cfg.EtherTalk.NetStart != a.cfg.EtherTalk.NetEnd {
// Pick a new network number at random
2024-04-07 12:09:58 +10:00
a.myAddr.Proto.Network = rand.N(
2024-04-06 17:46:00 +11:00
a.cfg.EtherTalk.NetEnd-a.cfg.EtherTalk.NetStart+1,
) + a.cfg.EtherTalk.NetStart
}
2024-04-12 15:20:04 +10:00
// Can't use: 0x00, 0xff, 0xfe, and should avoid the existing node number
2024-04-06 17:46:00 +11:00
newNode := rand.N[ddp.Node](0xfd) + 1
2024-04-06 18:20:59 +11:00
for newNode != a.myAddr.Proto.Node {
2024-04-06 17:46:00 +11:00
newNode = rand.N[ddp.Node](0xfd) + 1
}
2024-04-06 18:20:59 +11:00
a.myAddr.Proto.Node = newNode
2024-04-06 17:46:00 +11:00
a.probes = 0
}
// Send an AARP response
func (a *AARPMachine) heyThatsMe(targ aarp.AddrPair) error {
2024-04-13 18:21:22 +10:00
respFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Response(a.myAddr, targ))
2024-04-06 17:46:00 +11:00
if err != nil {
return err
}
2024-04-13 18:25:14 +10:00
//log.Printf("AARP: sending packet %+v", respFrame)
2024-04-13 17:59:24 +10:00
// Instead of broadcasting the reply, send it to the target specifically?
2024-04-13 18:21:22 +10:00
respFrame.Dst = targ.Hardware
2024-04-06 17:46:00 +11:00
respFrameRaw, err := ethertalk.Marshal(*respFrame)
if err != nil {
return err
}
return a.pcapHandle.WritePacketData(respFrameRaw)
}
// Broadcast an AARP Probe
func (a *AARPMachine) probe() error {
2024-04-06 18:20:59 +11:00
probeFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Probe(a.myAddr.Hardware, a.myAddr.Proto))
2024-04-06 17:46:00 +11:00
if err != nil {
return err
}
probeFrameRaw, err := ethertalk.Marshal(*probeFrame)
if err != nil {
return err
}
return a.pcapHandle.WritePacketData(probeFrameRaw)
}
2024-04-06 18:20:59 +11:00
// Broadcast an AARP Request
2024-04-07 12:09:58 +10:00
func (a *AARPMachine) request(ddpAddr ddp.Addr) error {
2024-04-06 18:20:59 +11:00
reqFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Request(a.myAddr, ddpAddr))
if err != nil {
return err
}
reqFrameRaw, err := ethertalk.Marshal(*reqFrame)
if err != nil {
return err
}
return a.pcapHandle.WritePacketData(reqFrameRaw)
}
2024-04-06 17:46:00 +11:00
type amtEntry struct {
2024-04-12 15:20:04 +10:00
hwAddr ethernet.Addr
last time.Time
updated chan struct{}
requesting bool
2024-04-06 17:46:00 +11:00
}
2024-04-07 12:09:58 +10:00
// addressMappingTable implements a concurrent-safe Address Mapping Table for
// AppleTalk (DDP) addresses to Ethernet hardware addresses.
type addressMappingTable struct {
mu sync.Mutex
2024-04-06 18:20:59 +11:00
table map[ddp.Addr]*amtEntry
2024-04-06 17:46:00 +11:00
}
// Learn adds or updates an AMT entry.
2024-04-07 12:09:58 +10:00
func (t *addressMappingTable) Learn(ddpAddr ddp.Addr, hwAddr ethernet.Addr) {
2024-04-06 17:46:00 +11:00
t.mu.Lock()
defer t.mu.Unlock()
if t.table == nil {
2024-04-06 18:20:59 +11:00
t.table = make(map[ddp.Addr]*amtEntry)
}
oldEnt := t.table[ddpAddr]
if oldEnt == nil {
t.table[ddpAddr] = &amtEntry{
2024-04-12 15:20:04 +10:00
hwAddr: hwAddr,
last: time.Now(),
updated: make(chan struct{}),
requesting: false,
2024-04-06 18:20:59 +11:00
}
return
}
oldEnt.hwAddr = hwAddr
oldEnt.last = time.Now()
2024-04-12 15:20:04 +10:00
oldEnt.requesting = false
2024-04-06 18:20:59 +11:00
close(oldEnt.updated)
oldEnt.updated = make(chan struct{})
}
2024-04-07 12:09:58 +10:00
// lookupOrWait returns either the valid cached Ethernet address for the given
2024-04-12 15:20:04 +10:00
// DDP address, or a non-nil channel that is closed when the entry is updated.
// It also reports if this is the first call since the entry became invalid.
func (t *addressMappingTable) lookupOrWait(ddpAddr ddp.Addr) (ethernet.Addr, <-chan struct{}, bool) {
2024-04-06 18:20:59 +11:00
t.mu.Lock()
defer t.mu.Unlock()
if t.table == nil {
t.table = make(map[ddp.Addr]*amtEntry)
}
2024-04-12 15:20:04 +10:00
ent := t.table[ddpAddr]
if ent == nil {
ch := make(chan struct{})
t.table[ddpAddr] = &amtEntry{
updated: ch,
requesting: true,
}
return ethernet.Addr{}, ch, true
2024-04-06 17:46:00 +11:00
}
2024-04-12 15:20:04 +10:00
if time.Since(ent.last) >= maxAMTEntryAge {
if ent.requesting {
return ent.hwAddr, ent.updated, false
}
ent.requesting = true
return ent.hwAddr, ent.updated, true
2024-04-06 17:46:00 +11:00
}
2024-04-12 15:20:04 +10:00
return ent.hwAddr, nil, false
2024-04-06 17:46:00 +11:00
}
2024-04-13 15:18:33 +10:00
func (t *addressMappingTable) requestingStopped(ddpAddr ddp.Addr) {
t.mu.Lock()
defer t.mu.Unlock()
if t.table == nil {
return
}
ent := t.table[ddpAddr]
if ent == nil {
return
}
ent.requesting = false
}