Multi-port refactor

This commit is contained in:
Josh Deprez 2024-05-03 16:13:59 +10:00
parent 545087fbe8
commit a16191d9e4
No known key found for this signature in database
12 changed files with 717 additions and 633 deletions

243
main.go
View file

@ -23,7 +23,6 @@ import (
"errors"
"flag"
"fmt"
"io"
"log"
"math/rand/v2"
"net"
@ -44,7 +43,6 @@ import (
"github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
)
const routingTableTemplate = `
@ -159,10 +157,10 @@ func main() {
ln, err := net.ListenUDP("udp4", &net.UDPAddr{Port: int(cfg.ListenPort)})
if err != nil {
log.Fatalf("Couldn't listen on udp4:387: %v", err)
log.Fatalf("AURP: Couldn't listen on udp4:387: %v", err)
}
defer ln.Close()
log.Printf("Listening on %v", ln.LocalAddr())
log.Printf("AURP: Listening on %v", ln.LocalAddr())
log.Println("Press ^C or send SIGINT to stop the router gracefully")
cctx, cancel := context.WithCancel(context.Background())
@ -203,7 +201,7 @@ func main() {
defer pcapHandle.Close()
// -------------------------------- Tables --------------------------------
routes := router.NewRoutingTable()
routes := router.NewRouteTable()
status.AddItem(ctx, "Routing table", routingTableTemplate, func(context.Context) (any, error) {
rs := routes.Dump()
slices.SortFunc(rs, func(ra, rb router.Route) int {
@ -213,7 +211,6 @@ func main() {
})
zones := router.NewZoneTable()
zones.Upsert(cfg.EtherTalk.NetStart, cfg.EtherTalk.ZoneName, true)
status.AddItem(ctx, "Zone table", zoneTableTemplate, func(context.Context) (any, error) {
zs := zones.Dump()
slices.SortFunc(zs, func(za, zb router.Zone) int {
@ -331,28 +328,39 @@ func main() {
// --------------------------------- AARP ---------------------------------
aarpMachine := router.NewAARPMachine(cfg, pcapHandle, myHWAddr)
aarpCh := make(chan *ethertalk.Packet, 1024)
go aarpMachine.Run(ctx, aarpCh)
go aarpMachine.Run(ctx)
// --------------------------------- RTMP ---------------------------------
rtmpMachine := &router.RTMPMachine{
AARP: aarpMachine,
AARPMachine: aarpMachine,
Config: cfg,
PcapHandle: pcapHandle,
RoutingTable: routes,
IncomingCh: make(chan *ddp.ExtPacket, 1024),
}
rtmpCh := make(chan *ddp.ExtPacket, 1024)
go rtmpMachine.Run(ctx, rtmpCh)
go rtmpMachine.Run(ctx)
// -------------------------------- Router --------------------------------
rooter := &router.Router{
Config: cfg,
PcapHandle: pcapHandle,
MyHWAddr: myHWAddr,
// MyDDPAddr: ...,
AARPMachine: aarpMachine,
RouteTable: routes,
ZoneTable: zones,
RouteTable: routes,
ZoneTable: zones,
}
etherTalkPort := &router.EtherTalkPort{
EthernetAddr: myHWAddr,
NetStart: cfg.EtherTalk.NetStart,
NetEnd: cfg.EtherTalk.NetEnd,
DefaultZoneName: cfg.EtherTalk.ZoneName,
AvailableZones: []string{cfg.EtherTalk.ZoneName},
PcapHandle: pcapHandle,
AARPMachine: aarpMachine,
RTMPMachine: rtmpMachine,
Router: rooter,
}
routes.InsertEtherTalkDirect(etherTalkPort)
for _, az := range etherTalkPort.AvailableZones {
zones.Upsert(etherTalkPort.NetStart, az, etherTalkPort)
}
// ---------------------- Raw AppleTalk/AARP inbound ----------------------
@ -360,145 +368,12 @@ func main() {
go func() {
defer wg.Done()
ctx, setStatus, done := status.AddSimpleItem(ctx, "EtherTalk inbound")
defer done()
ctx, setStatus, _ := status.AddSimpleItem(ctx, "EtherTalk inbound")
defer setStatus("EtherTalk Serve goroutine exited!")
setStatus(fmt.Sprintf("Listening on %s", cfg.EtherTalk.Device))
for {
if ctx.Err() != nil {
return
}
rawPkt, _, err := pcapHandle.ReadPacketData()
if errors.Is(err, pcap.NextErrorTimeoutExpired) {
continue
}
if errors.Is(err, io.EOF) || errors.Is(err, pcap.NextErrorNoMorePackets) {
return
}
if err != nil {
log.Printf("Couldn't read AppleTalk / AARP packet data: %v", err)
return
}
ethFrame := new(ethertalk.Packet)
if err := ethertalk.Unmarshal(rawPkt, ethFrame); err != nil {
log.Printf("Couldn't unmarshal EtherTalk frame: %v", err)
continue
}
// Ignore if sent by me
if ethFrame.Src == myHWAddr {
continue
}
switch ethFrame.SNAPProto {
case ethertalk.AARPProto:
// log.Print("Got an AARP frame")
aarpCh <- ethFrame
case ethertalk.AppleTalkProto:
// log.Print("Got an AppleTalk frame")
ddpkt := new(ddp.ExtPacket)
if err := ddp.ExtUnmarshal(ethFrame.Payload, ddpkt); err != nil {
log.Printf("Couldn't unmarshal DDP packet: %v", err)
continue
}
log.Printf("DDP: src (%d.%d s %d) dst (%d.%d s %d) proto %d data len %d",
ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket,
ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
ddpkt.Proto, len(ddpkt.Data))
// Glean address info for AMT, but only if SrcNet is our net
// (If it's not our net, then it was routed from elsewhere, and
// we'd be filling the AMT with entries for a router.)
if ddpkt.SrcNet >= cfg.EtherTalk.NetStart && ddpkt.SrcNet <= cfg.EtherTalk.NetEnd {
srcAddr := ddp.Addr{Network: ddpkt.SrcNet, Node: ddpkt.SrcNode}
aarpMachine.Learn(srcAddr, ethFrame.Src)
// log.Printf("DDP: Gleaned that %d.%d -> %v", srcAddr.Network, srcAddr.Node, ethFrame.Src)
}
// Packet for us? First, who am I?
myAddr, ok := aarpMachine.Address()
if !ok {
continue
}
rooter.MyDDPAddr = myAddr.Proto
// Our network?
// "The network number 0 is reserved to mean unknown; by default
// it specifies the local network to which the node is
// connected. Packets whose destination network number is 0 are
// addressed to a node on the local network."
// TODO: more generic routing
if ddpkt.DstNet != 0 && !(ddpkt.DstNet >= cfg.EtherTalk.NetStart && ddpkt.DstNet <= cfg.EtherTalk.NetEnd) {
// Is it for a network in the routing table?
route := routes.LookupRoute(ddpkt.DstNet)
if route == nil {
log.Printf("DDP: no route for network %d", ddpkt.DstNet)
continue
}
switch {
case route.AURPPeer != nil:
// Encap ethPacket.Payload into an AURP packet
log.Printf("DDP: forwarding to AURP peer %v", route.AURPPeer.RemoteAddr)
if _, err := route.AURPPeer.Send(route.AURPPeer.Transport.NewAppleTalkPacket(ethFrame.Payload)); err != nil {
log.Printf("DDP: Couldn't forward packet to AURP peer: %v", err)
}
case route.EtherTalkPeer != nil:
// Route payload to another router over EtherTalk
// TODO: this is unlikely because we currenly only support 1 EtherTalk port
log.Printf("DDP: forwarding to EtherTalk peer %v", route.EtherTalkPeer.PeerAddr)
// Note: resolving AARP can block
if err := route.EtherTalkPeer.Forward(ctx, ddpkt); err != nil {
log.Printf("DDP: Couldn't forward packet to EtherTalk peer: %v", err)
}
default:
log.Print("DDP: no forwarding mechanism for route; dropping packet")
}
continue
}
// To me?
// "Node ID 0 indicates any router on the network"- I'm a router
// "node ID $FF indicates either a network-wide or zone-specific
// broadcast"- that's relevant
if ddpkt.DstNode != 0 && ddpkt.DstNode != 0xff && ddpkt.DstNode != myAddr.Proto.Node {
continue
}
switch ddpkt.DstSocket {
case 1: // The RTMP socket
rtmpCh <- ddpkt
case 2: // The NIS (name information socket / NBP socket)
if err := rooter.HandleNBP(ethFrame.Src, ddpkt); err != nil {
log.Printf("NBP: Couldn't handle: %v", err)
}
case 4: // The AEP socket
if err := rooter.HandleAEP(ethFrame.Src, ddpkt); err != nil {
log.Printf("AEP: Couldn't handle: %v", err)
}
case 6: // The ZIS (zone information socket / ZIP socket)
if err := rooter.HandleZIP(ctx, ethFrame.Src, ddpkt); err != nil {
log.Printf("ZIP: couldn't handle: %v", err)
}
default:
log.Printf("DDP: No handler for socket %d", ddpkt.DstSocket)
}
default:
log.Printf("Read unknown packet %s -> %s with payload %x", ethFrame.Src, ethFrame.Dst, ethFrame.Payload)
}
}
etherTalkPort.Serve(ctx)
}()
// ----------------------------- AURP inbound -----------------------------
@ -598,74 +473,24 @@ func main() {
ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
ddpkt.Proto, len(ddpkt.Data))
// Route the packet
// Check and adjust the Hop Count
// Note the ddp package doesn't make this simple
hopCount := (ddpkt.Size & 0x3C00) >> 10
if hopCount >= 15 {
log.Printf("DDP/AURP: hop count exceeded (%d >= 15)", hopCount)
continue
}
hopCount++
ddpkt.Size &^= 0x3C00
ddpkt.Size |= hopCount << 10
if ddpkt.DstNet < cfg.EtherTalk.NetStart || ddpkt.DstNet > cfg.EtherTalk.NetEnd {
// Is it a network in the routing table?
route := routes.LookupRoute(ddpkt.DstNet)
if route == nil {
log.Printf("DDP/AURP: no route for packet (dstnet %d); dropping packet", ddpkt.DstNet)
break
}
switch {
case route.AURPPeer != nil:
// Routing between AURP peers... bit weird but OK
log.Printf("DDP/AURP: forwarding to AURP peer %v", route.AURPPeer.RemoteAddr)
outPkt, err := ddp.ExtMarshal(*ddpkt)
if err != nil {
log.Printf("DDP/AURP: Couldn't re-marshal packet: %v", err)
break
}
if _, err := route.AURPPeer.Send(route.AURPPeer.Transport.NewAppleTalkPacket(outPkt)); err != nil {
log.Printf("DDP/AURP: Couldn't forward packet to AURP peer: %v", err)
}
case route.EtherTalkPeer != nil:
// AURP peer -> EtherTalk peer
// Note: resolving AARP can block
log.Printf("DDP/AURP: forwarding to EtherTalk peer %v", route.EtherTalkPeer.PeerAddr)
if err := route.EtherTalkPeer.Forward(ctx, ddpkt); err != nil {
log.Printf("DDP/AURP: Couldn't forward packet to EtherTalk peer: %v", err)
}
default:
log.Print("DDP/AURP: no forwarding mechanism for route; dropping packet")
}
continue
}
// Is it addressed to me? Is it NBP?
// Is it addressed to me?
if ddpkt.DstNode == 0 { // Node 0 = any router for the network = me
// Is it NBP? FwdReq needs translating.
if ddpkt.DstSocket != 2 {
// Something else?? TODO
log.Printf("DDP/AURP: I don't have anything 'listening' on socket %d", ddpkt.DstSocket)
continue
}
// It's NBP
if err := rooter.HandleNBPInAURP(pr, ddpkt); err != nil {
if err := rooter.HandleNBPInAURP(ctx, pr, ddpkt); err != nil {
log.Printf("NBP/DDP/AURP: %v", err)
}
continue
}
// Note: resolving AARP can block
if err := rooter.SendEtherTalkDDP(ctx, ddpkt); err != nil {
log.Printf("DDP/AURP: couldn't send Ethertalk out: %v", err)
// Route the packet
if err := rooter.Forward(ctx, ddpkt); err != nil {
log.Printf("DDP/AURP: Couldn't route packet: %v", err)
}
continue
default:
log.Printf("AURP: Got unknown packet type %v", dh.PacketType)

View file

@ -73,6 +73,8 @@ type AARPMachine struct {
cfg *Config
pcapHandle *pcap.Handle
incomingCh chan *ethertalk.Packet
// 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
@ -90,6 +92,7 @@ func NewAARPMachine(cfg *Config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr
addressMappingTable: new(addressMappingTable),
cfg: cfg,
pcapHandle: pcapHandle,
incomingCh: make(chan *ethertalk.Packet, 1024), // arbitrary capacity
myAddr: aarp.AddrPair{
Hardware: myHWAddr,
},
@ -97,6 +100,14 @@ func NewAARPMachine(cfg *Config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr
}
}
// Handle handles a packet.
func (a *AARPMachine) Handle(ctx context.Context, pkt *ethertalk.Packet) {
select {
case <-ctx.Done():
case a.incomingCh <- pkt:
}
}
// 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) {
@ -123,7 +134,7 @@ func (a *AARPMachine) status(ctx context.Context) (any, error) {
}
// Run executes the machine.
func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Packet) error {
func (a *AARPMachine) Run(ctx context.Context) error {
ctx, done := status.AddItem(ctx, "AARP", aarpStatusTemplate, a.status)
defer done()
@ -165,9 +176,9 @@ func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Pack
log.Printf("Couldn't broadcast a Probe: %v", err)
}
case ethFrame, ok := <-incomingCh:
case ethFrame, ok := <-a.incomingCh:
if !ok {
incomingCh = nil
a.incomingCh = nil
}
var aapkt aarp.Packet

View file

@ -17,14 +17,14 @@
package router
import (
"context"
"fmt"
"gitea.drjosh.dev/josh/jrouter/atalk/aep"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
)
func (rtr *Router) HandleAEP(src ethernet.Addr, ddpkt *ddp.ExtPacket) error {
func (rtr *Router) HandleAEP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
if ddpkt.Proto != ddp.ProtoAEP {
return fmt.Errorf("invalid DDP type %d on socket 4", ddpkt.Proto)
}
@ -47,7 +47,7 @@ func (rtr *Router) HandleAEP(src ethernet.Addr, ddpkt *ddp.ExtPacket) error {
ddpkt.DstSocket, ddpkt.SrcSocket = ddpkt.SrcSocket, ddpkt.DstSocket
ddpkt.Data[0] = byte(aep.EchoReply)
return rtr.sendEtherTalkDDP(src, ddpkt)
return rtr.Forward(ctx, ddpkt)
default:
return fmt.Errorf("invalid AEP function %d", ep.Function)

View file

@ -17,16 +17,16 @@
package router
import (
"context"
"fmt"
"log"
"gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/nbp"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
)
func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) error {
func (port *EtherTalkPort) HandleNBP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
if ddpkt.Proto != ddp.ProtoNBP {
return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto)
}
@ -41,23 +41,27 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
switch nbpkt.Function {
case nbp.FunctionLkUp:
// when in AppleTalk, do as Apple Internet Router does...
outDDP, err := rtr.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, &nbpkt.Tuples[0])
outDDP, err := port.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, &nbpkt.Tuples[0])
if err != nil || outDDP == nil {
return err
}
log.Print("NBP: Replying to LkUp with LkUp-Reply for myself")
return rtr.sendEtherTalkDDP(srcHWAddr, outDDP)
// Note: AARP can block
return port.Send(ctx, outDDP)
case nbp.FunctionFwdReq:
// TODO: handle FwdReq in
case nbp.FunctionBrRq:
// There must be 1!
tuple := &nbpkt.Tuples[0]
zones := rtr.ZoneTable.LookupName(tuple.Zone)
zones := port.Router.ZoneTable.LookupName(tuple.Zone)
for _, z := range zones {
if z.Local {
// If it's for the local zone, translate it to a LkUp and broadcast it back
// out the EtherTalk port.
if outPort := z.LocalPort; outPort != nil {
// If it's for a local zone, translate it to a LkUp and broadcast
// out the corresponding EtherTalk port.
// "Note: On an internet, nodes on extended networks performing lookups in
// their own zone must replace a zone name of asterisk (*) with their actual
// zone name before sending the packet to A-ROUTER. All nodes performing
@ -86,35 +90,26 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
}
log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple)
if err := rtr.ZoneMulticastEtherTalkDDP(tuple.Zone, &outDDP); err != nil {
if err := outPort.ZoneMulticast(tuple.Zone, &outDDP); err != nil {
return err
}
// But also...if we match the query, reply as though it was a LkUp
outDDP2, err := rtr.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, tuple)
outDDP2, err := outPort.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, tuple)
if err != nil {
return err
}
if outDDP2 == nil {
continue
}
log.Print("NBP: Replying to BrRq with LkUp-Reply for myself")
if err := rtr.sendEtherTalkDDP(srcHWAddr, outDDP2); err != nil {
log.Print("NBP: Replying to BrRq directly with LkUp-Reply for myself")
if err := port.Router.Forward(ctx, outDDP2); err != nil {
return err
}
continue
}
route := rtr.RouteTable.LookupRoute(z.Network)
if route == nil {
return fmt.Errorf("no route for network %d", z.Network)
}
peer := route.AURPPeer
if peer == nil {
return fmt.Errorf("nil peer for route for network %d", z.Network)
}
// Translate it into a FwdReq and route it to the
// routers with the appropriate zone(s).
nbpkt.Function = nbp.FunctionFwdReq
@ -123,7 +118,7 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
return fmt.Errorf("couldn't marshal FwdReq: %v", err)
}
outDDP := ddp.ExtPacket{
outDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)),
Cksum: 0,
@ -138,16 +133,7 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
Data: nbpRaw,
}
outDDPRaw, err := ddp.ExtMarshal(outDDP)
if err != nil {
return err
}
log.Printf("NBP: Sending FwdReq to %v for tuple %v", peer.RemoteAddr, tuple)
if _, err := peer.Send(peer.Transport.NewAppleTalkPacket(outDDPRaw)); err != nil {
return fmt.Errorf("sending FwdReq on to peer: %w", err)
}
return port.Router.Forward(ctx, outDDP)
}
default:
@ -157,14 +143,15 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
return nil
}
func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *nbp.Tuple) (*ddp.ExtPacket, error) {
// Returns an NBP LkUp-Reply for the router itself, with the address from this port.
func (port *EtherTalkPort) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *nbp.Tuple) (*ddp.ExtPacket, error) {
if tuple.Object != "jrouter" && tuple.Object != "=" {
return nil, nil
}
if tuple.Type != "AppleRouter" && tuple.Type != "=" {
return nil, nil
}
if tuple.Zone != rtr.Config.EtherTalk.ZoneName && tuple.Zone != "*" && tuple.Zone != "" {
if tuple.Zone != port.DefaultZoneName && tuple.Zone != "*" && tuple.Zone != "" {
return nil, nil
}
respPkt := &nbp.Packet{
@ -172,13 +159,13 @@ func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *
NBPID: nbpID,
Tuples: []nbp.Tuple{
{
Network: rtr.MyDDPAddr.Network,
Node: rtr.MyDDPAddr.Node,
Network: port.MyAddr.Network,
Node: port.MyAddr.Node,
Socket: 253,
Enumerator: 0,
Object: "jrouter",
Type: "AppleRouter",
Zone: rtr.Config.EtherTalk.ZoneName,
Zone: port.DefaultZoneName,
},
},
}
@ -193,8 +180,8 @@ func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *
DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket,
SrcNet: rtr.MyDDPAddr.Network,
SrcNode: rtr.MyDDPAddr.Node,
SrcNet: port.MyAddr.Network,
SrcNode: port.MyAddr.Node,
SrcSocket: 2,
Proto: ddp.ProtoNBP,
},

View file

@ -17,6 +17,7 @@
package router
import (
"context"
"fmt"
"log"
@ -24,7 +25,7 @@ import (
"github.com/sfiera/multitalk/pkg/ddp"
)
func (rtr *Router) HandleNBPInAURP(peer *AURPPeer, ddpkt *ddp.ExtPacket) error {
func (rtr *Router) HandleNBPInAURP(ctx context.Context, peer *AURPPeer, ddpkt *ddp.ExtPacket) error {
if ddpkt.Proto != ddp.ProtoNBP {
return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto)
}
@ -42,43 +43,44 @@ func (rtr *Router) HandleNBPInAURP(peer *AURPPeer, ddpkt *ddp.ExtPacket) error {
}
tuple := &nbpkt.Tuples[0]
if tuple.Zone != rtr.Config.EtherTalk.ZoneName {
return fmt.Errorf("FwdReq querying zone %q, which is not our zone", tuple.Zone)
zones := rtr.ZoneTable.LookupName(tuple.Zone)
for _, z := range zones {
if z.LocalPort == nil {
continue
}
port := z.LocalPort
log.Printf("NBP/DDP/AURP: Converting FwdReq to LkUp (%v)", tuple)
// Convert it to a LkUp and broadcast on EtherTalk
nbpkt.Function = nbp.FunctionLkUp
nbpRaw, err := nbpkt.Marshal()
if err != nil {
return fmt.Errorf("couldn't marshal LkUp: %v", err)
}
// "If the destination network is extended, however, the router must also
// change the destination network number to $0000, so that the packet is
// received by all nodes on the network (within the correct zone multicast
// address)."
ddpkt.DstNet = 0x0000
ddpkt.DstNode = 0xFF // Broadcast node address within the dest network
ddpkt.Data = nbpRaw
if err := port.ZoneMulticast(tuple.Zone, ddpkt); err != nil {
return err
}
// But also... if it matches us, reply directly with a LkUp-Reply of our own
outDDP, err := port.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, tuple)
if err != nil || outDDP == nil {
return err
}
log.Print("NBP/DDP/AURP: Replying to BrRq with LkUp-Reply for myself")
if err := rtr.Forward(ctx, outDDP); err != nil {
return err
}
}
// TODO: Route the FwdReq to another router if it's not our zone
log.Printf("NBP/DDP/AURP: Converting FwdReq to LkUp (%v)", tuple)
// Convert it to a LkUp and broadcast on EtherTalk
nbpkt.Function = nbp.FunctionLkUp
nbpRaw, err := nbpkt.Marshal()
if err != nil {
return fmt.Errorf("couldn't marshal LkUp: %v", err)
}
// "If the destination network is extended, however, the router must also
// change the destination network number to $0000, so that the packet is
// received by all nodes on the network (within the correct zone multicast
// address)."
ddpkt.DstNet = 0x0000
ddpkt.DstNode = 0xFF // Broadcast node address within the dest network
ddpkt.Data = nbpRaw
if err := rtr.ZoneMulticastEtherTalkDDP(tuple.Zone, ddpkt); err != nil {
return err
}
// But also... if it matches us, reply directly with a LkUp-Reply of our own
outDDP, err := rtr.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, tuple)
if err != nil || outDDP == nil {
return err
}
log.Print("NBP/DDP/AURP: Replying to BrRq with LkUp-Reply for myself")
outDDPRaw, err := ddp.ExtMarshal(*outDDP)
if err != nil {
return err
}
_, err = peer.Send(peer.Transport.NewAppleTalkPacket(outDDPRaw))
return err
return nil
}

View file

@ -115,7 +115,7 @@ type AURPPeer struct {
ReceiveCh chan aurp.Packet
// Routing table (the peer will add/remove/update routes)
RoutingTable *RoutingTable
RoutingTable *RouteTable
// Zone table (the peer will add/remove/update zones)
ZoneTable *ZoneTable
@ -125,6 +125,15 @@ type AURPPeer struct {
sstate SenderState
}
func (p *AURPPeer) Forward(ddpkt *ddp.ExtPacket) error {
outPkt, err := ddp.ExtMarshal(*ddpkt)
if err != nil {
return err
}
_, err = p.Send(p.Transport.NewAppleTalkPacket(outPkt))
return err
}
func (p *AURPPeer) ReceiverState() ReceiverState {
p.mu.RLock()
defer p.mu.RUnlock()
@ -596,7 +605,7 @@ func (p *AURPPeer) Handle(ctx context.Context) error {
case *aurp.ZIRspPacket:
log.Printf("AURP Peer: Learned about these zones: %v", pkt.Zones)
for _, zt := range pkt.Zones {
p.ZoneTable.Upsert(ddp.Network(zt.Network), zt.Name, false)
p.ZoneTable.Upsert(ddp.Network(zt.Network), zt.Name, nil)
}
case *aurp.GDZLReqPacket:

190
router/port.go Normal file
View file

@ -0,0 +1,190 @@
/*
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.
*/
package router
import (
"context"
"errors"
"io"
"log"
"gitea.drjosh.dev/josh/jrouter/atalk"
"github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
)
// EtherTalkPort is all the data and helpers needed for EtherTalk on one port.
type EtherTalkPort struct {
EthernetAddr ethernet.Addr
NetStart ddp.Network
NetEnd ddp.Network
MyAddr ddp.Addr
DefaultZoneName string
AvailableZones []string
PcapHandle *pcap.Handle
AARPMachine *AARPMachine
RTMPMachine *RTMPMachine
Router *Router
}
func (port *EtherTalkPort) Serve(ctx context.Context) {
for {
if ctx.Err() != nil {
return
}
rawPkt, _, err := port.PcapHandle.ReadPacketData()
if errors.Is(err, pcap.NextErrorTimeoutExpired) {
continue
}
if errors.Is(err, io.EOF) || errors.Is(err, pcap.NextErrorNoMorePackets) {
return
}
if err != nil {
log.Printf("Couldn't read AppleTalk / AARP packet data: %v", err)
return
}
ethFrame := new(ethertalk.Packet)
if err := ethertalk.Unmarshal(rawPkt, ethFrame); err != nil {
log.Printf("Couldn't unmarshal EtherTalk frame: %v", err)
continue
}
// Ignore if sent by me
if ethFrame.Src == port.EthernetAddr {
continue
}
switch ethFrame.SNAPProto {
case ethertalk.AARPProto:
// log.Print("Got an AARP frame")
port.AARPMachine.Handle(ctx, ethFrame)
case ethertalk.AppleTalkProto:
// log.Print("Got an AppleTalk frame")
ddpkt := new(ddp.ExtPacket)
if err := ddp.ExtUnmarshal(ethFrame.Payload, ddpkt); err != nil {
log.Printf("Couldn't unmarshal DDP packet: %v", err)
continue
}
log.Printf("DDP: src (%d.%d s %d) dst (%d.%d s %d) proto %d data len %d",
ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket,
ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
ddpkt.Proto, len(ddpkt.Data))
// Glean address info for AMT, but only if SrcNet is our net
// (If it's not our net, then it was routed from elsewhere, and
// we'd be filling the AMT with entries for a router.)
if ddpkt.SrcNet >= port.NetStart && ddpkt.SrcNet <= port.NetEnd {
srcAddr := ddp.Addr{Network: ddpkt.SrcNet, Node: ddpkt.SrcNode}
port.AARPMachine.Learn(srcAddr, ethFrame.Src)
// log.Printf("DDP: Gleaned that %d.%d -> %v", srcAddr.Network, srcAddr.Node, ethFrame.Src)
}
// Packet for us? First, who am I?
myAddr, ok := port.AARPMachine.Address()
if !ok {
continue
}
// Our network?
// "The network number 0 is reserved to mean unknown; by default
// it specifies the local network to which the node is
// connected. Packets whose destination network number is 0 are
// addressed to a node on the local network."
// TODO: more generic routing
if ddpkt.DstNet != 0 && !(ddpkt.DstNet >= port.NetStart && ddpkt.DstNet <= port.NetEnd) {
// Is it for a network in the routing table?
if err := port.Router.Forward(ctx, ddpkt); err != nil {
log.Printf("DDP: Couldn't forward packet: %v", err)
}
continue
}
// To me?
// "Node ID 0 indicates any router on the network"- I'm a router
// "node ID $FF indicates either a network-wide or zone-specific
// broadcast"- that's relevant
if ddpkt.DstNode != 0 && ddpkt.DstNode != 0xff && ddpkt.DstNode != myAddr.Proto.Node {
continue
}
switch ddpkt.DstSocket {
case 1: // The RTMP socket
port.RTMPMachine.Handle(ctx, ddpkt)
case 2: // The NIS (name information socket / NBP socket)
if err := port.HandleNBP(ctx, ddpkt); err != nil {
log.Printf("NBP: Couldn't handle: %v", err)
}
case 4: // The AEP socket
if err := port.Router.HandleAEP(ctx, ddpkt); err != nil {
log.Printf("AEP: Couldn't handle: %v", err)
}
case 6: // The ZIS (zone information socket / ZIP socket)
if err := port.HandleZIP(ctx, ddpkt); err != nil {
log.Printf("ZIP: couldn't handle: %v", err)
}
default:
log.Printf("DDP: No handler for socket %d", ddpkt.DstSocket)
}
default:
log.Printf("Read unknown packet %s -> %s with payload %x", ethFrame.Src, ethFrame.Dst, ethFrame.Payload)
}
}
}
func (port *EtherTalkPort) Send(ctx context.Context, pkt *ddp.ExtPacket) error {
dstEth := ethertalk.AppleTalkBroadcast
if pkt.DstNode != 0xFF {
de, err := port.AARPMachine.Resolve(ctx, ddp.Addr{Network: pkt.DstNet, Node: pkt.DstNode})
if err != nil {
return err
}
dstEth = de
}
return port.send(dstEth, pkt)
}
func (port *EtherTalkPort) Broadcast(pkt *ddp.ExtPacket) error {
return port.send(ethertalk.AppleTalkBroadcast, pkt)
}
func (port *EtherTalkPort) ZoneMulticast(zone string, pkt *ddp.ExtPacket) error {
return port.send(atalk.MulticastAddr(zone), pkt)
}
func (port *EtherTalkPort) send(dstEth ethernet.Addr, pkt *ddp.ExtPacket) error {
outFrame, err := ethertalk.AppleTalk(port.EthernetAddr, *pkt)
if err != nil {
return err
}
outFrame.Dst = dstEth
outFrameRaw, err := ethertalk.Marshal(*outFrame)
if err != nil {
return err
}
return port.PcapHandle.WritePacketData(outFrameRaw)
}

View file

@ -35,8 +35,9 @@ type Route struct {
LastSeen time.Time
// Exactly one of the following should be set
AURPPeer *AURPPeer
EtherTalkPeer *EtherTalkPeer
AURPPeer *AURPPeer // Next hop is this peer router (over AURP)
EtherTalkPeer *EtherTalkPeer // Next hop is this peer router (over EtherTalk)
EtherTalkDirect *EtherTalkPort // Directly connected to this network (via EtherTalk)
}
func (r Route) LastSeenAgo() string {
@ -46,18 +47,33 @@ func (r Route) LastSeenAgo() string {
return fmt.Sprintf("%v ago", time.Since(r.LastSeen).Truncate(time.Millisecond))
}
type RoutingTable struct {
type RouteTable struct {
mu sync.Mutex
routes map[*Route]struct{}
}
func NewRoutingTable() *RoutingTable {
return &RoutingTable{
func NewRouteTable() *RouteTable {
return &RouteTable{
routes: make(map[*Route]struct{}),
}
}
func (rt *RoutingTable) Dump() []Route {
func (rt *RouteTable) InsertEtherTalkDirect(port *EtherTalkPort) {
r := &Route{
Extended: true,
NetStart: port.NetStart,
NetEnd: port.NetEnd,
Distance: 0, // we're connected directly
LastSeen: time.Now(),
EtherTalkDirect: port,
}
rt.mu.Lock()
defer rt.mu.Unlock()
rt.routes[r] = struct{}{}
}
func (rt *RouteTable) Dump() []Route {
rt.mu.Lock()
defer rt.mu.Unlock()
@ -68,7 +84,7 @@ func (rt *RoutingTable) Dump() []Route {
return table
}
func (rt *RoutingTable) LookupRoute(network ddp.Network) *Route {
func (rt *RouteTable) LookupRoute(network ddp.Network) *Route {
rt.mu.Lock()
defer rt.mu.Unlock()
@ -92,7 +108,7 @@ func (rt *RoutingTable) LookupRoute(network ddp.Network) *Route {
return bestRoute
}
func (rt *RoutingTable) DeleteAURPPeer(peer *AURPPeer) {
func (rt *RouteTable) DeleteAURPPeer(peer *AURPPeer) {
rt.mu.Lock()
defer rt.mu.Unlock()
@ -103,7 +119,7 @@ func (rt *RoutingTable) DeleteAURPPeer(peer *AURPPeer) {
}
}
func (rt *RoutingTable) DeleteAURPPeerNetwork(peer *AURPPeer, network ddp.Network) {
func (rt *RouteTable) DeleteAURPPeerNetwork(peer *AURPPeer, network ddp.Network) {
rt.mu.Lock()
defer rt.mu.Unlock()
@ -114,7 +130,7 @@ func (rt *RoutingTable) DeleteAURPPeerNetwork(peer *AURPPeer, network ddp.Networ
}
}
func (rt *RoutingTable) UpdateAURPRouteDistance(peer *AURPPeer, network ddp.Network, distance uint8) {
func (rt *RouteTable) UpdateAURPRouteDistance(peer *AURPPeer, network ddp.Network, distance uint8) {
rt.mu.Lock()
defer rt.mu.Unlock()
@ -126,7 +142,7 @@ func (rt *RoutingTable) UpdateAURPRouteDistance(peer *AURPPeer, network ddp.Netw
}
}
func (rt *RoutingTable) UpsertEthRoute(peer *EtherTalkPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
func (rt *RouteTable) UpsertEthRoute(peer *EtherTalkPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
if netStart > netEnd {
return fmt.Errorf("invalid network range [%d, %d]", netStart, netEnd)
}
@ -169,7 +185,7 @@ func (rt *RoutingTable) UpsertEthRoute(peer *EtherTalkPeer, extended bool, netSt
return nil
}
func (rt *RoutingTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
func (rt *RouteTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
if netStart > netEnd {
return fmt.Errorf("invalid network range [%d, %d]", netStart, netEnd)
}
@ -192,7 +208,7 @@ func (rt *RoutingTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart,
return nil
}
func (rt *RoutingTable) ValidRoutes() []*Route {
func (rt *RouteTable) ValidRoutes() []*Route {
rt.mu.Lock()
defer rt.mu.Unlock()
valid := make([]*Route, 0, len(rt.routes))

View file

@ -18,53 +18,50 @@ package router
import (
"context"
"fmt"
"gitea.drjosh.dev/josh/jrouter/atalk"
"github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
)
type Router struct {
Config *Config
PcapHandle *pcap.Handle
MyHWAddr ethernet.Addr
MyDDPAddr ddp.Addr
AARPMachine *AARPMachine
RouteTable *RoutingTable
ZoneTable *ZoneTable
Config *Config
RouteTable *RouteTable
ZoneTable *ZoneTable
}
func (rtr *Router) SendEtherTalkDDP(ctx context.Context, pkt *ddp.ExtPacket) error {
dstEth := ethertalk.AppleTalkBroadcast
if pkt.DstNode != 0xFF {
de, err := rtr.AARPMachine.Resolve(ctx, ddp.Addr{Network: pkt.DstNet, Node: pkt.DstNode})
if err != nil {
return err
}
dstEth = de
// Forward routes a packet towards the right destination.
// It increments the hop count, then looks up the best route for the network,
// then transmits the packet according to the route.
func (rtr *Router) Forward(ctx context.Context, ddpkt *ddp.ExtPacket) error {
// Check and adjust the Hop Count
// Note the ddp package doesn't make this simple
hopCount := (ddpkt.Size & 0x3C00) >> 10
if hopCount >= 15 {
return fmt.Errorf("hop count exceeded (%d >= 15)", hopCount)
}
return rtr.sendEtherTalkDDP(dstEth, pkt)
}
hopCount++
ddpkt.Size &^= 0x3C00
ddpkt.Size |= hopCount << 10
func (rtr *Router) BroadcastEtherTalkDDP(pkt *ddp.ExtPacket) error {
return rtr.sendEtherTalkDDP(ethertalk.AppleTalkBroadcast, pkt)
}
switch route := rtr.RouteTable.LookupRoute(ddpkt.DstNet); {
case route == nil:
return fmt.Errorf("no route for packet (dstnet %d); dropping packet", ddpkt.DstNet)
func (rtr *Router) ZoneMulticastEtherTalkDDP(zone string, pkt *ddp.ExtPacket) error {
return rtr.sendEtherTalkDDP(atalk.MulticastAddr(zone), pkt)
}
case route.AURPPeer != nil:
// log.Printf("Forwarding packet to AURP peer %v", route.AURPPeer.RemoteAddr)
return route.AURPPeer.Forward(ddpkt)
func (rtr *Router) sendEtherTalkDDP(dstEth ethernet.Addr, pkt *ddp.ExtPacket) error {
outFrame, err := ethertalk.AppleTalk(rtr.MyHWAddr, *pkt)
if err != nil {
return err
case route.EtherTalkPeer != nil:
// log.Printf("Forwarding to EtherTalk peer %v", route.EtherTalkPeer.PeerAddr)
// Note: resolving AARP can block
return route.EtherTalkPeer.Forward(ctx, ddpkt)
case route.EtherTalkDirect != nil:
// log.Printf("Outputting to EtherTalk directly")
// Note: resolving AARP can block
return route.EtherTalkDirect.Send(ctx, ddpkt)
default:
return fmt.Errorf("no forwarding mechanism for route! %+v", route)
}
outFrame.Dst = dstEth
outFrameRaw, err := ethertalk.Marshal(*outFrame)
if err != nil {
return err
}
return rtr.PcapHandle.WritePacketData(outFrameRaw)
}

View file

@ -36,22 +36,33 @@ import (
// RTMPMachine implements RTMP on an AppleTalk network attached to the router.
type RTMPMachine struct {
AARP *AARPMachine
AARPMachine *AARPMachine
Config *Config
PcapHandle *pcap.Handle
RoutingTable *RoutingTable
RoutingTable *RouteTable
IncomingCh chan *ddp.ExtPacket
}
func (m *RTMPMachine) Handle(ctx context.Context, pkt *ddp.ExtPacket) {
select {
case <-ctx.Done():
case m.IncomingCh <- pkt:
}
}
// Run executes the machine.
func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket) error {
ctx, setStatus, done := status.AddSimpleItem(ctx, "RTMP")
defer done()
func (m *RTMPMachine) Run(ctx context.Context) (err error) {
ctx, setStatus, _ := status.AddSimpleItem(ctx, "RTMP")
defer func() {
setStatus(fmt.Sprintf("Run loop stopped! Return: %v", err))
}()
setStatus("Awaiting DDP address assignment")
// Await local address assignment before doing anything
<-m.AARP.Assigned()
myAddr, ok := m.AARP.Address()
<-m.AARPMachine.Assigned()
myAddr, ok := m.AARPMachine.Address()
if !ok {
return fmt.Errorf("AARP machine closed Assigned channel but Address is not valid")
}
@ -63,7 +74,7 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
}
setStatus("Packet loop")
setStatus("Starting packet loop")
bcastTicker := time.NewTicker(10 * time.Second)
defer bcastTicker.Stop()
@ -74,11 +85,13 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
return ctx.Err()
case <-bcastTicker.C:
setStatus("Broadcasting RTMP Data")
if err := m.broadcastData(myAddr); err != nil {
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
}
case pkt := <-incomingCh:
case pkt := <-m.IncomingCh:
setStatus("Handling incoming packet")
switch pkt.Proto {
case ddp.ProtoRTMPReq:
// I can answer RTMP requests!
@ -88,7 +101,7 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
}
// should be in the cache...
theirHWAddr, err := m.AARP.Resolve(ctx, ddp.Addr{Network: pkt.SrcNet, Node: pkt.SrcNode})
theirHWAddr, err := m.AARPMachine.Resolve(ctx, ddp.Addr{Network: pkt.SrcNet, Node: pkt.SrcNode})
if err != nil {
log.Printf("RTMP: Couldn't resolve %d.%d to a hardware address: %v", pkt.SrcNet, pkt.SrcNode, err)
continue
@ -173,8 +186,8 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
}
peer := &EtherTalkPeer{
PcapHandle: m.PcapHandle,
MyHWAddr: m.AARP.myAddr.Hardware,
AARP: m.AARP,
MyHWAddr: m.AARPMachine.myAddr.Hardware,
AARP: m.AARPMachine,
PeerAddr: dataPkt.RouterAddr,
}

View file

@ -20,266 +20,300 @@ import (
"context"
"fmt"
"log"
"slices"
"gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/atp"
"gitea.drjosh.dev/josh/jrouter/atalk/zip"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
)
func (rtr *Router) HandleZIP(ctx context.Context, srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) error {
func (port *EtherTalkPort) HandleZIP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
switch ddpkt.Proto {
case ddp.ProtoATP:
atpkt, err := atp.UnmarshalPacket(ddpkt.Data)
if err != nil {
return err
}
switch atpkt := atpkt.(type) {
case *atp.TReq:
gzl, err := zip.UnmarshalTReq(atpkt)
if err != nil {
return err
}
if gzl.StartIndex == 0 {
return fmt.Errorf("ZIP ATP: received request with StartIndex = 0 (invalid)")
}
resp := &zip.GetZonesReplyPacket{
TID: gzl.TID,
LastFlag: true,
}
switch gzl.Function {
case zip.FunctionGetZoneList:
resp.Zones = rtr.ZoneTable.AllNames()
case zip.FunctionGetLocalZones:
resp.Zones = rtr.ZoneTable.LocalNames()
case zip.FunctionGetMyZone:
resp.Zones = []string{rtr.Config.EtherTalk.ZoneName}
}
// Inside AppleTalk SE, pp 8-8
if int(gzl.StartIndex) > len(resp.Zones) {
// "Note: A 0-byte response will be returned by a router if the
// index specified in the request is greater than the index of
// the last zone in the list (and the user bytes field will
// indicate no more zones)."
resp.Zones = nil
} else {
// Trim the zones list
// "zone names in the router are assumed to be numbered starting
// with 1"
resp.Zones = resp.Zones[gzl.StartIndex-1:]
size := 0
for i, z := range resp.Zones {
size += 1 + len(z) // length prefix plus string
if size > atp.MaxDataSize {
resp.LastFlag = false
resp.Zones = resp.Zones[:i]
break
}
}
}
respATP, err := resp.MarshalTResp()
if err != nil {
return err
}
ddpBody, err := respATP.Marshal()
if err != nil {
return err
}
respDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: uint16(len(ddpBody)) + atalk.DDPExtHeaderSize,
Cksum: 0,
DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket,
SrcNet: rtr.MyDDPAddr.Network,
SrcNode: rtr.MyDDPAddr.Node,
SrcSocket: 6,
Proto: ddp.ProtoATP,
},
Data: ddpBody,
}
return rtr.sendEtherTalkDDP(srcHWAddr, respDDP)
case *atp.TResp:
return fmt.Errorf("TODO: support handling ZIP ATP replies?")
default:
return fmt.Errorf("unsupported ATP packet type %T for ZIP", atpkt)
}
return port.handleZIPATP(ctx, ddpkt)
case ddp.ProtoZIP:
zipkt, err := zip.UnmarshalPacket(ddpkt.Data)
if err != nil {
return err
}
switch zipkt := zipkt.(type) {
case *zip.QueryPacket:
log.Printf("ZIP: Got Query for networks %v", zipkt.Networks)
networks := rtr.ZoneTable.Query(zipkt.Networks)
sendReply := func(resp *zip.ReplyPacket) error {
respRaw, err := resp.Marshal()
if err != nil {
return fmt.Errorf("couldn't marshal %T: %w", resp, err)
}
outDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0,
DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket,
SrcNet: rtr.MyDDPAddr.Network,
SrcNode: rtr.MyDDPAddr.Node,
SrcSocket: 6,
Proto: ddp.ProtoZIP,
},
Data: respRaw,
}
return rtr.sendEtherTalkDDP(srcHWAddr, outDDP)
}
// Inside AppleTalk SE, pp 8-11:
//
// "Replies (but not Extended Replies) can contain any number of
// zones lists, as long as the zones list for each network is
// entirely contained in the Reply packet."
//
// and
//
// "The zones list for a given network must be contiguous in the
// packet, with each zone name in that list preceded by the first
// network number in the range of the requested network."
size := 2
for _, zl := range networks {
for _, z := range zl {
size += 3 + len(z) // Network number, length byte, string
}
}
if size <= atalk.DDPMaxDataSize {
// Send one non-extended reply packet with all the data
log.Printf("ZIP: Replying with non-extended Reply: %v", networks)
return sendReply(&zip.ReplyPacket{
Extended: false,
// "Replies contain the number of zones lists indicated in
// the Reply header."
NetworkCount: uint8(len(networks)),
Networks: networks,
})
}
// Send Extended Reply packets, 1 or more for each network
//
// "Extended Replies can contain only one zones list."
for nn, zl := range networks {
rem := zl // rem: remaining zone names to send for this network
for len(rem) > 0 {
size := 2
var chunk []string // chunk: zone names to send now
for _, z := range rem {
size += 3 + len(z)
if size > atalk.DDPMaxDataSize {
break
}
chunk = append(chunk, z)
}
rem = rem[len(chunk):]
nets := map[ddp.Network][]string{
nn: chunk,
}
log.Printf("ZIP: Replying with Extended Reply: %v", nets)
err := sendReply(&zip.ReplyPacket{
Extended: true,
// "The network count in the header indicates, not the
// number of zones names in the packet, but the number
// of zone names in the entire zones list for the
// requested network, which may span more than one
// packet."
NetworkCount: uint8(len(zl)),
Networks: nets,
})
if err != nil {
return err
}
}
}
return nil
case *zip.GetNetInfoPacket:
log.Printf("ZIP: Got GetNetInfo for zone %q", zipkt.ZoneName)
// Only running a network with one zone for now.
resp := &zip.GetNetInfoReplyPacket{
ZoneInvalid: zipkt.ZoneName != rtr.Config.EtherTalk.ZoneName,
UseBroadcast: false,
OnlyOneZone: true,
NetStart: rtr.Config.EtherTalk.NetStart,
NetEnd: rtr.Config.EtherTalk.NetEnd,
ZoneName: zipkt.ZoneName, // has to match request
MulticastAddr: atalk.MulticastAddr(rtr.Config.EtherTalk.ZoneName),
DefaultZoneName: rtr.Config.EtherTalk.ZoneName,
}
log.Printf("ZIP: Replying with GetNetInfo-Reply: %+v", resp)
respRaw, err := resp.Marshal()
if err != nil {
return fmt.Errorf("couldn't marshal %T: %w", resp, err)
}
// "In cases where a node's provisional address is
// invalid, routers will not be able to respond to
// the node in a directed manner. An address is
// invalid if the network number is neither in the
// startup range nor in the network number range
// assigned to the node's network. In these cases,
// if the request was sent via a broadcast, the
// routers should respond with a broadcast."
outDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0,
DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket,
SrcNet: rtr.MyDDPAddr.Network,
SrcNode: rtr.MyDDPAddr.Node,
SrcSocket: 6,
Proto: ddp.ProtoZIP,
},
Data: respRaw,
}
if ddpkt.DstNet == 0x0000 {
outDDP.DstNet = 0x0000
}
if ddpkt.DstNode == 0xFF {
outDDP.DstNode = 0xFF
}
// If it's a broadcast packet, broadcast it. Otherwise don't
dstEth := ethertalk.AppleTalkBroadcast
if outDDP.DstNode != 0xFF {
dstEth = srcHWAddr
}
return rtr.sendEtherTalkDDP(dstEth, outDDP)
default:
return fmt.Errorf("TODO: handle type %T", zipkt)
}
return port.handleZIPZIP(ctx, ddpkt)
default:
return fmt.Errorf("invalid DDP type %d on socket 6", ddpkt.Proto)
}
}
func (port *EtherTalkPort) handleZIPZIP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
zipkt, err := zip.UnmarshalPacket(ddpkt.Data)
if err != nil {
return err
}
switch zipkt := zipkt.(type) {
case *zip.QueryPacket:
return port.handleZIPQuery(ctx, ddpkt, zipkt)
case *zip.GetNetInfoPacket:
return port.handleZIPGetNetInfo(ctx, ddpkt, zipkt)
default:
return fmt.Errorf("TODO: handle type %T", zipkt)
}
}
func (port *EtherTalkPort) handleZIPQuery(ctx context.Context, ddpkt *ddp.ExtPacket, zipkt *zip.QueryPacket) error {
log.Printf("ZIP: Got Query for networks %v", zipkt.Networks)
networks := port.Router.ZoneTable.Query(zipkt.Networks)
sendReply := func(resp *zip.ReplyPacket) error {
respRaw, err := resp.Marshal()
if err != nil {
return fmt.Errorf("couldn't marshal %T: %w", resp, err)
}
outDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0,
DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket,
SrcNet: port.MyAddr.Network,
SrcNode: port.MyAddr.Node,
SrcSocket: 6,
Proto: ddp.ProtoZIP,
},
Data: respRaw,
}
// Note: AARP can block
return port.Send(ctx, outDDP)
}
// Inside AppleTalk SE, pp 8-11:
//
// "Replies (but not Extended Replies) can contain any number of
// zones lists, as long as the zones list for each network is
// entirely contained in the Reply packet."
//
// and
//
// "The zones list for a given network must be contiguous in the
// packet, with each zone name in that list preceded by the first
// network number in the range of the requested network."
size := 2
for _, zl := range networks {
for _, z := range zl {
size += 3 + len(z) // Network number, length byte, string
}
}
if size <= atalk.DDPMaxDataSize {
// Send one non-extended reply packet with all the data
log.Printf("ZIP: Replying with non-extended Reply: %v", networks)
return sendReply(&zip.ReplyPacket{
Extended: false,
// "Replies contain the number of zones lists indicated in
// the Reply header."
NetworkCount: uint8(len(networks)),
Networks: networks,
})
}
// Send Extended Reply packets, 1 or more for each network
//
// "Extended Replies can contain only one zones list."
for nn, zl := range networks {
rem := zl // rem: remaining zone names to send for this network
for len(rem) > 0 {
size := 2
var chunk []string // chunk: zone names to send now
for _, z := range rem {
size += 3 + len(z)
if size > atalk.DDPMaxDataSize {
break
}
chunk = append(chunk, z)
}
rem = rem[len(chunk):]
nets := map[ddp.Network][]string{
nn: chunk,
}
log.Printf("ZIP: Replying with Extended Reply: %v", nets)
err := sendReply(&zip.ReplyPacket{
Extended: true,
// "The network count in the header indicates, not the
// number of zones names in the packet, but the number
// of zone names in the entire zones list for the
// requested network, which may span more than one
// packet."
NetworkCount: uint8(len(zl)),
Networks: nets,
})
if err != nil {
return err
}
}
}
return nil
}
func (port *EtherTalkPort) handleZIPGetNetInfo(ctx context.Context, ddpkt *ddp.ExtPacket, zipkt *zip.GetNetInfoPacket) error {
log.Printf("ZIP: Got GetNetInfo for zone %q", zipkt.ZoneName)
// The request is valid if the zone name is available on this network.
valid := slices.Contains(port.AvailableZones, zipkt.ZoneName)
// The multicast address we return depends on the validity of the zone
// name.
var mcastAddr ethernet.Addr
if valid {
mcastAddr = atalk.MulticastAddr(zipkt.ZoneName)
} else {
mcastAddr = atalk.MulticastAddr(port.DefaultZoneName)
}
resp := &zip.GetNetInfoReplyPacket{
ZoneInvalid: valid,
UseBroadcast: false,
OnlyOneZone: len(port.AvailableZones) == 1,
NetStart: port.NetStart,
NetEnd: port.NetEnd,
ZoneName: zipkt.ZoneName, // has to match request
MulticastAddr: mcastAddr,
}
// The default zone name is only returned if the requested zone name is
// invalid.
if !valid {
resp.DefaultZoneName = port.DefaultZoneName
}
log.Printf("ZIP: Replying with GetNetInfo-Reply: %+v", resp)
respRaw, err := resp.Marshal()
if err != nil {
return fmt.Errorf("couldn't marshal %T: %w", resp, err)
}
// "In cases where a node's provisional address is
// invalid, routers will not be able to respond to
// the node in a directed manner. An address is
// invalid if the network number is neither in the
// startup range nor in the network number range
// assigned to the node's network. In these cases,
// if the request was sent via a broadcast, the
// routers should respond with a broadcast."
outDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0,
DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket,
SrcNet: port.MyAddr.Network,
SrcNode: port.MyAddr.Node,
SrcSocket: 6,
Proto: ddp.ProtoZIP,
},
Data: respRaw,
}
// If it arrived as a broadcast, send the reply as a broadcast.
if ddpkt.DstNet == 0x0000 {
outDDP.DstNet = 0x0000
}
if ddpkt.DstNode == 0xFF {
outDDP.DstNode = 0xFF
}
// Note: AARP can block
return port.Send(ctx, outDDP)
}
func (port *EtherTalkPort) handleZIPATP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
atpkt, err := atp.UnmarshalPacket(ddpkt.Data)
if err != nil {
return err
}
switch atpkt := atpkt.(type) {
case *atp.TReq:
return port.handleZIPTReq(ctx, ddpkt, atpkt)
case *atp.TResp:
return fmt.Errorf("TODO: support handling ZIP ATP replies?")
default:
return fmt.Errorf("unsupported ATP packet type %T for ZIP", atpkt)
}
}
func (port *EtherTalkPort) handleZIPTReq(ctx context.Context, ddpkt *ddp.ExtPacket, atpkt *atp.TReq) error {
gzl, err := zip.UnmarshalTReq(atpkt)
if err != nil {
return err
}
if gzl.StartIndex == 0 {
return fmt.Errorf("ZIP ATP: received request with StartIndex = 0 (invalid)")
}
resp := &zip.GetZonesReplyPacket{
TID: gzl.TID,
LastFlag: true,
}
switch gzl.Function {
case zip.FunctionGetZoneList:
resp.Zones = port.Router.ZoneTable.AllNames()
case zip.FunctionGetLocalZones:
resp.Zones = port.AvailableZones
case zip.FunctionGetMyZone:
// Note: This shouldn't happen on extended networks (e.g. EtherTalk)
resp.Zones = []string{port.DefaultZoneName}
}
// Inside AppleTalk SE, pp 8-8
if int(gzl.StartIndex) > len(resp.Zones) {
// "Note: A 0-byte response will be returned by a router if the
// index specified in the request is greater than the index of
// the last zone in the list (and the user bytes field will
// indicate no more zones)."
resp.Zones = nil
} else {
// Trim the zones list
// "zone names in the router are assumed to be numbered starting
// with 1"
// and note we checked for 0 above
resp.Zones = resp.Zones[gzl.StartIndex-1:]
size := 0
for i, z := range resp.Zones {
size += 1 + len(z) // length prefix plus string
if size > atp.MaxDataSize {
resp.LastFlag = false
resp.Zones = resp.Zones[:i]
break
}
}
}
respATP, err := resp.MarshalTResp()
if err != nil {
return err
}
ddpBody, err := respATP.Marshal()
if err != nil {
return err
}
respDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: uint16(len(ddpBody)) + atalk.DDPExtHeaderSize,
Cksum: 0,
DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket,
SrcNet: port.MyAddr.Network,
SrcNode: port.MyAddr.Node,
SrcSocket: 6,
Proto: ddp.ProtoATP,
},
Data: ddpBody,
}
// Note: AARP can block
return port.Send(ctx, respDDP)
}

View file

@ -29,10 +29,10 @@ import (
//const maxZoneAge = 10 * time.Minute // TODO: confirm
type Zone struct {
Network ddp.Network
Name string
Local bool
LastSeen time.Time
Network ddp.Network
Name string
LocalPort *EtherTalkPort // nil if remote (local to another router)
LastSeen time.Time
}
func (z Zone) LastSeenAgo() string {
@ -68,21 +68,21 @@ func (zt *ZoneTable) Dump() []Zone {
return zs
}
func (zt *ZoneTable) Upsert(network ddp.Network, name string, local bool) {
func (zt *ZoneTable) Upsert(network ddp.Network, name string, localPort *EtherTalkPort) {
zt.mu.Lock()
defer zt.mu.Unlock()
key := zoneKey{network, name}
z := zt.zones[key]
if z != nil {
z.Local = local
z.LocalPort = localPort
z.LastSeen = time.Now()
return
}
zt.zones[key] = &Zone{
Network: network,
Name: name,
Local: local,
LastSeen: time.Now(),
Network: network,
Name: name,
LocalPort: localPort,
LastSeen: time.Now(),
}
}
@ -116,29 +116,29 @@ func (zt *ZoneTable) LookupName(name string) []*Zone {
return zs
}
func (zt *ZoneTable) LocalNames() []string {
zt.mu.Lock()
seen := make(map[string]struct{})
zs := make([]string, 0, len(zt.zones))
for _, z := range zt.zones {
// if time.Since(z.LastSeen) > maxZoneAge {
// continue
// }
if !z.Local {
continue
}
if _, s := seen[z.Name]; s {
continue
}
seen[z.Name] = struct{}{}
zs = append(zs, z.Name)
// func (zt *ZoneTable) LocalNames() []string {
// zt.mu.Lock()
// seen := make(map[string]struct{})
// zs := make([]string, 0, len(zt.zones))
// for _, z := range zt.zones {
// // if time.Since(z.LastSeen) > maxZoneAge {
// // continue
// // }
// if z.Local != nil {
// continue
// }
// if _, s := seen[z.Name]; s {
// continue
// }
// seen[z.Name] = struct{}{}
// zs = append(zs, z.Name)
}
zt.mu.Unlock()
// }
// zt.mu.Unlock()
sort.Strings(zs)
return zs
}
// sort.Strings(zs)
// return zs
// }
func (zt *ZoneTable) AllNames() []string {
zt.mu.Lock()