jrouter/router/aarp.go

479 lines
12 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-19 14:57:25 +10:00
package router
2024-04-06 17:46:00 +11:00
import (
"context"
2024-04-23 14:15:41 +10:00
"fmt"
2024-04-06 17:46:00 +11:00
"log"
"math/rand/v2"
"sync"
"time"
2024-04-23 14:15:41 +10:00
"gitea.drjosh.dev/josh/jrouter/status"
2024-04-06 17:46:00 +11:00
"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
aarpBodyLength = 28 // bytes
2024-04-06 18:20:59 +11:00
)
2024-04-06 17:46:00 +11:00
2024-04-26 11:51:27 +10:00
const aarpStatusTemplate = `
Status: {{.Status}}<br/>
<table>
<thead><tr>
<th>DDP addr</th>
<th>Ethernet addr</th>
2024-04-26 12:16:43 +10:00
<th>Valid?
2024-04-26 11:51:27 +10:00
<th>Last updated</th>
<th>Being resolved?</th>
</tr></thead>
<tbody>
{{range $key, $entry := .AMT}}
<tr>
<td>{{$key.Network}}.{{$key.Node}}</td>
<td>{{$entry.HWAddr}}</td>
2024-04-26 12:16:43 +10:00
<td>{{if $entry.Valid}}{{else}}{{end}}</td>
<td>{{$entry.LastUpdatedAgo}}</td>
2024-04-26 12:39:49 +10:00
<td>{{if $entry.Resolving}}{{else}}💤{{end}}</td>
2024-04-26 11:51:27 +10:00
</tr>
{{end}}
</tbody>
</table>
`
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
2024-04-19 14:57:25 +10:00
cfg *Config
2024-04-06 17:46:00 +11:00
pcapHandle *pcap.Handle
2024-05-03 16:13:59 +10:00
incomingCh chan *ethertalk.Packet
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
2024-04-26 11:51:27 +10:00
statusMsg string
2024-04-07 17:01:26 +10:00
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-19 14:57:25 +10:00
func NewAARPMachine(cfg *Config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr) *AARPMachine {
2024-04-06 20:11:15 +11:00
return &AARPMachine{
2024-04-07 12:09:58 +10:00
addressMappingTable: new(addressMappingTable),
cfg: cfg,
pcapHandle: pcapHandle,
2024-05-03 16:13:59 +10:00
incomingCh: make(chan *ethertalk.Packet, 1024), // arbitrary capacity
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-05-03 16:13:59 +10:00
// Handle handles a packet.
func (a *AARPMachine) Handle(ctx context.Context, pkt *ethertalk.Packet) {
select {
case <-ctx.Done():
case a.incomingCh <- pkt:
}
}
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-26 11:51:27 +10:00
func (a *AARPMachine) status(ctx context.Context) (any, error) {
a.mu.RLock()
defer a.mu.RUnlock()
return struct {
Status string
AMT map[ddp.Addr]AMTEntry
}{
Status: a.statusMsg,
AMT: a.addressMappingTable.Dump(),
}, nil
}
2024-04-07 12:09:58 +10:00
// Run executes the machine.
2024-05-03 16:13:59 +10:00
func (a *AARPMachine) Run(ctx context.Context) error {
2024-04-26 11:51:27 +10:00
ctx, done := status.AddItem(ctx, "AARP", aarpStatusTemplate, a.status)
2024-04-23 14:15:41 +10:00
defer done()
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()
2024-04-26 11:51:27 +10:00
a.statusMsg = "Initialising"
2024-04-07 12:09:58 +10:00
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()
2024-04-26 11:51:27 +10:00
a.statusMsg = fmt.Sprintf("Assigned address %d.%d", a.myAddr.Proto.Network, a.myAddr.Proto.Node)
2024-04-12 15:20:04 +10:00
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()
2024-04-26 11:51:27 +10:00
a.statusMsg = fmt.Sprintf("Probed %d times", a.probes)
2024-04-07 12:09:58 +10:00
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
}
2024-05-03 16:13:59 +10:00
case ethFrame, ok := <-a.incomingCh:
2024-04-06 17:46:00 +11:00
if !ok {
2024-05-03 16:13:59 +10:00
a.incomingCh = nil
2024-04-06 17:46:00 +11:00
}
// sfiera/multitalk will return an "excess data" error if the
// payload is too big. Most traffic I've seen locally does not have
// this problem, but I've seen one report with some junk trailing
// data on AARP packets.
payload := ethFrame.Payload
if len(payload) > aarpBodyLength {
payload = payload[:aarpBodyLength]
}
2024-04-06 17:46:00 +11:00
var aapkt aarp.Packet
if err := aarp.Unmarshal(payload, &aapkt); err != nil {
2024-04-06 17:46:00 +11:00
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-26 11:51:27 +10:00
// AMTEntry is an entry in an address mapping table.
type AMTEntry struct {
// The hardware address that the entry maps to.
HWAddr ethernet.Addr
// The last time this entry was updated.
LastUpdated time.Time
// Whether the address is being resolved.
Resolving bool
// Closed when this entry is updated.
updated chan struct{}
2024-04-06 17:46:00 +11:00
}
2024-04-26 12:16:43 +10:00
// Valid reports if the entry is valid.
2024-04-26 12:18:17 +10:00
func (e AMTEntry) Valid() bool {
return time.Since(e.LastUpdated) < maxAMTEntryAge
2024-04-26 12:16:43 +10:00
}
// LastUpdatedAgo is a friendly string reporting how long ago the entry was
// updated/resolved.
2024-04-26 12:18:17 +10:00
func (e AMTEntry) LastUpdatedAgo() string {
2024-05-12 18:12:27 +10:00
return ago(e.LastUpdated)
2024-04-26 12:16:43 +10: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-26 11:51:27 +10:00
table map[ddp.Addr]*AMTEntry
}
// Dump returns a copy of the table at a point in time.
func (t *addressMappingTable) Dump() map[ddp.Addr]AMTEntry {
t.mu.Lock()
defer t.mu.Unlock()
table := make(map[ddp.Addr]AMTEntry, len(t.table))
for k, v := range t.table {
table[k] = *v
}
return table
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-26 11:51:27 +10:00
t.table = make(map[ddp.Addr]*AMTEntry)
2024-04-06 18:20:59 +11:00
}
oldEnt := t.table[ddpAddr]
if oldEnt == nil {
2024-04-26 11:51:27 +10:00
t.table[ddpAddr] = &AMTEntry{
HWAddr: hwAddr,
LastUpdated: time.Now(),
updated: make(chan struct{}),
Resolving: false,
2024-04-06 18:20:59 +11:00
}
return
}
2024-04-26 11:51:27 +10:00
oldEnt.HWAddr = hwAddr
oldEnt.LastUpdated = time.Now()
oldEnt.Resolving = 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 {
2024-04-26 11:51:27 +10:00
t.table = make(map[ddp.Addr]*AMTEntry)
2024-04-06 18:20:59 +11:00
}
2024-04-12 15:20:04 +10:00
ent := t.table[ddpAddr]
if ent == nil {
ch := make(chan struct{})
2024-04-26 11:51:27 +10:00
t.table[ddpAddr] = &AMTEntry{
updated: ch,
Resolving: true,
2024-04-12 15:20:04 +10:00
}
return ethernet.Addr{}, ch, true
2024-04-06 17:46:00 +11:00
}
2024-04-26 12:16:43 +10:00
if !ent.Valid() {
2024-04-26 11:51:27 +10:00
if ent.Resolving {
return ent.HWAddr, ent.updated, false
2024-04-12 15:20:04 +10:00
}
2024-04-26 11:51:27 +10:00
ent.Resolving = true
return ent.HWAddr, ent.updated, true
2024-04-06 17:46:00 +11:00
}
2024-04-26 11:51:27 +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
}
2024-04-26 11:51:27 +10:00
ent.Resolving = false
2024-04-13 15:18:33 +10:00
}