diff --git a/aarp.go b/aarp.go index 7d5d3c6..dffc321 100644 --- a/aarp.go +++ b/aarp.go @@ -14,8 +14,12 @@ import ( "github.com/sfiera/multitalk/pkg/ethertalk" ) -// TODO: verify this parameter -const maxAMTEntryAge = 30 * time.Second +const ( + // TODO: verify parameters + maxAMTEntryAge = 30 * time.Second + aarpRequestRetransmit = 1 * time.Second + aarpRequestTimeout = 10 * time.Second +) // AARPMachine maintains both an Address Mapping Table and handles AARP packets // (sending and receiving requests, responses, and probes). This process assumes @@ -30,8 +34,7 @@ type AARPMachine struct { state aarpState probes int - myHWAddr ethernet.Addr - myDDPAddr ddp.Addr + myAddr aarp.AddrPair } type aarpState int @@ -49,7 +52,7 @@ func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Pack a.probes = 0 // Initialise our DDP address with a preferred address (first network.1) - a.myDDPAddr = ddp.Addr{ + a.myAddr.Proto = ddp.Addr{ Network: ddp.Network(a.cfg.EtherTalk.NetStart), Node: 1, } @@ -95,7 +98,7 @@ func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Pack 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 { + if aapkt.Dst.Proto != a.myAddr.Proto { continue } if a.state != aarpStateAssigned { @@ -111,7 +114,7 @@ func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Pack 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 { + if aapkt.Dst.Proto != a.myAddr.Proto { continue } if a.state == aarpStateProbing { @@ -122,7 +125,7 @@ func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Pack 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 { + if aapkt.Dst.Proto != a.myAddr.Proto { continue } switch a.state { @@ -142,30 +145,69 @@ func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Pack } } +// 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) { + // try the cache first + result, ok := a.AMT.Lookup(ddpAddr) + if ok { + return result, nil + } + + if err := a.request(ddpAddr); err != nil { + return ethernet.Addr{}, err + } + + ticker := time.NewTicker(aarpRequestRetransmit) + defer ticker.Stop() + + ctx, cancel := context.WithTimeout(ctx, aarpRequestTimeout) + defer cancel() + + for { + // We might have a result already + result, ok := a.AMT.Lookup(ddpAddr) + if ok { + return result, nil + } + + select { + case <-ctx.Done(): + return ethernet.Addr{}, ctx.Err() + + case <-a.AMT.Wait(ddpAddr): + // Should have a result now. + + case <-ticker.C: + if err := a.request(ddpAddr); err != nil { + return ethernet.Addr{}, err + } + } + } +} + // 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.myAddr.Proto.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 { + for newNode != a.myAddr.Proto.Node { newNode = rand.N[ddp.Node](0xfd) + 1 } - a.myDDPAddr.Node = newNode + a.myAddr.Proto.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, - })) + respFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Response(targ, a.myAddr)) if err != nil { return err } @@ -180,7 +222,7 @@ func (a *AARPMachine) heyThatsMe(targ aarp.AddrPair) error { // Broadcast an AARP Probe func (a *AARPMachine) probe() error { - probeFrame, err := ethertalk.AARP(a.myHWAddr, aarp.Probe(a.myHWAddr, a.myDDPAddr)) + probeFrame, err := ethertalk.AARP(a.myAddr.Hardware, aarp.Probe(a.myAddr.Hardware, a.myAddr.Proto)) if err != nil { return err } @@ -191,16 +233,30 @@ func (a *AARPMachine) probe() error { return a.pcapHandle.WritePacketData(probeFrameRaw) } +// Broadcast an AARP Request +func (a AARPMachine) request(ddpAddr ddp.Addr) error { + 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) +} + type amtEntry struct { - hwAddr ethernet.Addr - last time.Time + hwAddr ethernet.Addr + last time.Time + updated chan struct{} } // 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 + table map[ddp.Addr]*amtEntry } // Learn adds or updates an AMT entry. @@ -208,12 +264,44 @@ 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 = make(map[ddp.Addr]*amtEntry) } - t.table[ddpAddr] = amtEntry{ - hwAddr: hwAddr, - last: time.Now(), + oldEnt := t.table[ddpAddr] + if oldEnt == nil { + t.table[ddpAddr] = &amtEntry{ + hwAddr: hwAddr, + last: time.Now(), + updated: make(chan struct{}), + } + return } + + if oldEnt.hwAddr == hwAddr && time.Since(oldEnt.last) < maxAMTEntryAge { + oldEnt.last = time.Now() + return + } + oldEnt.hwAddr = hwAddr + oldEnt.last = time.Now() + close(oldEnt.updated) + oldEnt.updated = make(chan struct{}) +} + +// Wait returns a channel that is closed when the entry for ddpAddr is updated. +func (t *AMT) Wait(ddpAddr ddp.Addr) <-chan struct{} { + t.mu.Lock() + defer t.mu.Unlock() + if t.table == nil { + t.table = make(map[ddp.Addr]*amtEntry) + } + oldEnt := t.table[ddpAddr] + if oldEnt != nil { + return oldEnt.updated + } + ch := make(chan struct{}) + t.table[ddpAddr] = &amtEntry{ + updated: ch, + } + return ch } // Lookup searches for a non-expired entry in the table only. It does not send diff --git a/main.go b/main.go index bd70bcf..ebee46c 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ 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" @@ -154,7 +155,9 @@ func main() { AMT: new(AMT), cfg: cfg, pcapHandle: pcapHandle, - myHWAddr: myHWAddr, + myAddr: aarp.AddrPair{ + Hardware: myHWAddr, + }, } aarpCh := make(chan *ethertalk.Packet, 1024) go aarpMachine.Run(ctx, aarpCh) diff --git a/peer.go b/peer.go index 2dd91d6..ebb40d4 100644 --- a/peer.go +++ b/peer.go @@ -256,8 +256,8 @@ func (p *peer) handle(ctx context.Context) error { nets := aurp.NetworkTuples{ { Extended: true, - RangeStart: p.cfg.EtherTalk.NetStart, - RangeEnd: p.cfg.EtherTalk.NetEnd, + RangeStart: uint16(p.cfg.EtherTalk.NetStart), + RangeEnd: uint16(p.cfg.EtherTalk.NetEnd), Distance: 0, }, } @@ -312,7 +312,7 @@ func (p *peer) handle(ctx context.Context) error { if pkt.Flags&aurp.RoutingFlagSendZoneInfo != 0 { zones := aurp.ZoneTuples{ { - Network: p.cfg.EtherTalk.NetStart, + Network: uint16(p.cfg.EtherTalk.NetStart), Name: p.cfg.EtherTalk.ZoneName, }, } @@ -346,7 +346,7 @@ func (p *peer) handle(ctx context.Context) error { // ZI-Req zones := aurp.ZoneTuples{ { - Network: p.cfg.EtherTalk.NetStart, + Network: uint16(p.cfg.EtherTalk.NetStart), Name: p.cfg.EtherTalk.ZoneName, }, }