diff --git a/main.go b/main.go index b17fac3..f7e17ff 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/router/aarp.go b/router/aarp.go index 7397b7a..2fdeb11 100644 --- a/router/aarp.go +++ b/router/aarp.go @@ -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 diff --git a/router/aep.go b/router/aep.go index 40c692a..1a8f9b4 100644 --- a/router/aep.go +++ b/router/aep.go @@ -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) diff --git a/router/nbp.go b/router/nbp.go index ec7a7e5..46881c9 100644 --- a/router/nbp.go +++ b/router/nbp.go @@ -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, }, diff --git a/router/nbp_aurp.go b/router/nbp_aurp.go index 306751e..eb84b48 100644 --- a/router/nbp_aurp.go +++ b/router/nbp_aurp.go @@ -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 } diff --git a/router/peer_aurp.go b/router/peer_aurp.go index d36eb56..85432cc 100644 --- a/router/peer_aurp.go +++ b/router/peer_aurp.go @@ -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: diff --git a/router/port.go b/router/port.go new file mode 100644 index 0000000..26e3e3c --- /dev/null +++ b/router/port.go @@ -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) +} diff --git a/router/route.go b/router/route.go index f629821..ffaf259 100644 --- a/router/route.go +++ b/router/route.go @@ -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)) diff --git a/router/router.go b/router/router.go index 916e3a7..784147d 100644 --- a/router/router.go +++ b/router/router.go @@ -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) } diff --git a/router/rtmp.go b/router/rtmp.go index c7aea62..04b96ce 100644 --- a/router/rtmp.go +++ b/router/rtmp.go @@ -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, } diff --git a/router/zip.go b/router/zip.go index ded821f..92599e6 100644 --- a/router/zip.go +++ b/router/zip.go @@ -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) +} diff --git a/router/zones.go b/router/zones.go index c05f6ff..f1d0d74 100644 --- a/router/zones.go +++ b/router/zones.go @@ -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()