diff --git a/atalk/nbp/nbp.go b/atalk/nbp/nbp.go index 423533d..c2a948c 100644 --- a/atalk/nbp/nbp.go +++ b/atalk/nbp/nbp.go @@ -101,7 +101,7 @@ func Unmarshal(data []byte) (*Packet, error) { t := Tuple{ Network: ddp.Network(binary.BigEndian.Uint16(data[:2])), Node: ddp.Node(data[2]), - Socket: data[3], + Socket: ddp.Socket(data[3]), Enumerator: data[4], } data = data[5:] @@ -128,7 +128,7 @@ func Unmarshal(data []byte) (*Packet, error) { type Tuple struct { Network ddp.Network Node ddp.Node - Socket uint8 + Socket ddp.Socket Enumerator uint8 Object string // length-prefixed Type string // length-prefixed @@ -147,7 +147,7 @@ func (t *Tuple) writeTo(b *bytes.Buffer) error { } write16(b, t.Network) b.WriteByte(byte(t.Node)) - b.WriteByte(t.Socket) + b.WriteByte(byte(t.Socket)) b.WriteByte(t.Enumerator) b.WriteByte(byte(len(t.Object))) b.WriteString(t.Object) diff --git a/main.go b/main.go index b17fac3..59932e7 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 = ` @@ -63,7 +61,17 @@ const routingTableTemplate = ` {{if $route.Extended}}✅{{else}}❌{{end}} {{$route.Distance}} {{$route.LastSeenAgo}} - {{if $route.AURPPeer}}{{$route.AURPPeer.RemoteAddr}}{{else if $route.EtherTalkPeer}}{{$route.EtherTalkPeer.PeerAddr.Network}}.{{$route.EtherTalkPeer.PeerAddr.Node}}{{else}}-{{end}} + + {{- with $route.AURPPeer -}} + {{.RemoteAddr}} + {{- end -}} + {{- with $route.EtherTalkPeer -}} + {{.Port.Device}} {{.PeerAddr.Network}}.{{.PeerAddr.Node}} + {{- end -}} + {{- with $route.EtherTalkDirect -}} + {{.Device}} {{.NetStart}}-{{.NetEnd}} + {{- end -}} + {{end}} @@ -75,7 +83,7 @@ const zoneTableTemplate = ` Network Name - Local + Local Port Last seen @@ -83,7 +91,7 @@ const zoneTableTemplate = ` {{$zone.Network}} {{$zone.Name}} - {{if $zone.Local}}✅{{else}}❌{{end}} + {{with $zone.LocalPort}}{{.Device}}{{else}}-{{end}} {{$zone.LastSeenAgo}} {{end}} @@ -159,10 +167,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 +211,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 +221,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,174 +338,46 @@ func main() { // --------------------------------- AARP --------------------------------- aarpMachine := router.NewAARPMachine(cfg, pcapHandle, myHWAddr) - aarpCh := make(chan *ethertalk.Packet, 1024) - go aarpMachine.Run(ctx, aarpCh) - - // --------------------------------- RTMP --------------------------------- - rtmpMachine := &router.RTMPMachine{ - AARP: aarpMachine, - Config: cfg, - PcapHandle: pcapHandle, - RoutingTable: routes, - } - rtmpCh := make(chan *ddp.ExtPacket, 1024) - go rtmpMachine.Run(ctx, rtmpCh) + go aarpMachine.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{ + Device: cfg.EtherTalk.Device, + EthernetAddr: myHWAddr, + NetStart: cfg.EtherTalk.NetStart, + NetEnd: cfg.EtherTalk.NetEnd, + DefaultZoneName: cfg.EtherTalk.ZoneName, + AvailableZones: []string{cfg.EtherTalk.ZoneName}, + PcapHandle: pcapHandle, + AARPMachine: aarpMachine, + Router: rooter, + } + rooter.Ports = append(rooter.Ports, etherTalkPort) + routes.InsertEtherTalkDirect(etherTalkPort) + for _, az := range etherTalkPort.AvailableZones { + zones.Upsert(etherTalkPort.NetStart, az, etherTalkPort) + } + + // --------------------------------- RTMP --------------------------------- + go etherTalkPort.RunRTMP(ctx) + // ---------------------- Raw AppleTalk/AARP inbound ---------------------- wg.Add(1) 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 ----------------------------- @@ -593,79 +472,37 @@ func main() { log.Printf("AURP: Couldn't unmarshal encapsulated DDP packet: %v", err) continue } - log.Printf("DDP/AURP: Got %d.%d.%d -> %d.%d.%d proto %d data len %d", - ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket, - ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket, - ddpkt.Proto, len(ddpkt.Data)) + // log.Printf("DDP/AURP: Got %d.%d.%d -> %d.%d.%d proto %d data len %d", + // ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket, + // 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) + // Is it addressed to me? + var localPort *router.EtherTalkPort + for _, port := range rooter.Ports { + if ddpkt.DstNet >= port.NetStart && ddpkt.DstNet <= port.NetEnd { + localPort = port 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? - if ddpkt.DstNode == 0 { // Node 0 = any router for the network = me + if ddpkt.DstNode == 0 && localPort != nil { // 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 { + // It's NBP, specifically it should be a FwdReq + if err := rooter.HandleNBPFromAURP(ctx, 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..8a3d99b 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.Output(ctx, ddpkt) default: return fmt.Errorf("invalid AEP function %d", ep.Function) diff --git a/router/nbp.go b/router/nbp.go index ec7a7e5..673b9d3 100644 --- a/router/nbp.go +++ b/router/nbp.go @@ -17,16 +17,17 @@ package router import ( + "context" "fmt" "log" + "slices" "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,130 +42,180 @@ 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(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: + return port.Router.handleNBPFwdReq(ctx, ddpkt, nbpkt) case nbp.FunctionBrRq: - // There must be 1! - tuple := &nbpkt.Tuples[0] + return port.handleNBPBrRq(ctx, ddpkt, nbpkt) - zones := rtr.ZoneTable.LookupName(tuple.Zone) + default: + return fmt.Errorf("TODO: handle function %v", nbpkt.Function) + } +} - 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. - // "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 - // lookups in their own zone will receive LkUp packets from themselves - // (actually sent by a router). The node's NBP process should expect to - // receive these packets and must reply to them." - nbpkt.Function = nbp.FunctionLkUp - nbpRaw, err := nbpkt.Marshal() - if err != nil { - return fmt.Errorf("couldn't marshal LkUp: %v", err) - } +func (port *EtherTalkPort) handleNBPBrRq(ctx context.Context, ddpkt *ddp.ExtPacket, nbpkt *nbp.Packet) error { + // A BrRq was addressed to us. The sender (on a local network) is aware that + // the network is extended and routed, and instead of broadcasting a LkUp + // itself, is asking us to do it. - outDDP := ddp.ExtPacket{ - ExtHeader: ddp.ExtHeader{ - Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)), - Cksum: 0, - SrcNet: ddpkt.SrcNet, - SrcNode: ddpkt.SrcNode, - SrcSocket: ddpkt.SrcSocket, - DstNet: 0x0000, // Local network broadcast - DstNode: 0xFF, // Broadcast node address within the dest network - DstSocket: 2, - Proto: ddp.ProtoNBP, - }, - Data: nbpRaw, - } + // There must be 1! + tuple := &nbpkt.Tuples[0] - log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple) - if err := rtr.ZoneMulticastEtherTalkDDP(tuple.Zone, &outDDP); err != nil { - return err - } + // This logic would be required on a non-extended network: + // if tuple.Zone == "" || tuple.Zone == "*" { + // tuple.Zone = port.DefaultZoneName + // } - // But also...if we match the query, reply as though it was a LkUp - outDDP2, err := rtr.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 { - return err - } + zones := port.Router.ZoneTable.LookupName(tuple.Zone) - 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 + for _, z := range zones { + 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 + // lookups in their own zone will receive LkUp packets from themselves + // (actually sent by a router). The node's NBP process should expect to + // receive these packets and must reply to them." + nbpkt.Function = nbp.FunctionLkUp nbpRaw, err := nbpkt.Marshal() if err != nil { - return fmt.Errorf("couldn't marshal FwdReq: %v", err) + return fmt.Errorf("couldn't marshal LkUp: %v", err) } outDDP := ddp.ExtPacket{ ExtHeader: ddp.ExtHeader{ Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)), Cksum: 0, - SrcNet: ddpkt.SrcNet, - SrcNode: ddpkt.SrcNode, - SrcSocket: ddpkt.SrcSocket, - DstNet: z.Network, - DstNode: 0x00, // Any router for the dest network + SrcNet: port.MyAddr.Network, + SrcNode: port.MyAddr.Node, + SrcSocket: 2, + DstNet: 0x0000, // Local network broadcast + DstNode: 0xFF, // Broadcast node address within the dest network DstSocket: 2, Proto: ddp.ProtoNBP, }, Data: nbpRaw, } - outDDPRaw, err := ddp.ExtMarshal(outDDP) - if err != nil { + log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple) + if err := outPort.ZoneMulticast(tuple.Zone, &outDDP); 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) + // But also...if we match the query, reply as though it was a LkUp + // This uses the *input* port information. + outDDP2, err := port.helloWorldThisIsMe(nbpkt.NBPID, tuple) + if err != nil { + return err } + if outDDP2 == nil { + continue + } + log.Print("NBP: Replying to BrRq directly with LkUp-Reply for myself") + // Can reply to this BrRq on the same port we got it, because it + // wasn't routed + if err := port.Send(ctx, outDDP2); err != nil { + return err + } + + continue } - default: - return fmt.Errorf("TODO: handle function %v", nbpkt.Function) - } + // The zone table row is *not* for a local network. + // Translate it into a FwdReq and route that to the routers that do have + // that zone as a local network. + nbpkt.Function = nbp.FunctionFwdReq + nbpRaw, err := nbpkt.Marshal() + if err != nil { + return fmt.Errorf("couldn't marshal FwdReq: %v", err) + } + outDDP := &ddp.ExtPacket{ + ExtHeader: ddp.ExtHeader{ + Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)), + Cksum: 0, + SrcNet: ddpkt.SrcNet, + SrcNode: ddpkt.SrcNode, + SrcSocket: ddpkt.SrcSocket, + DstNet: z.Network, + DstNode: 0x00, // Any router for the dest network + DstSocket: 2, + Proto: ddp.ProtoNBP, + }, + Data: nbpRaw, + } + + if err := port.Router.Output(ctx, outDDP); err != nil { + return err + } + } return nil } -func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *nbp.Tuple) (*ddp.ExtPacket, error) { +func (rtr *Router) handleNBPFwdReq(ctx context.Context, ddpkt *ddp.ExtPacket, nbpkt *nbp.Packet) error { + // A FwdReq was addressed to us. That means a remote router thinks the + // zone is available on one or more of our local networks. + + // There must be 1! + tuple := &nbpkt.Tuples[0] + + for _, outPort := range rtr.Ports { + if !slices.Contains(outPort.AvailableZones, tuple.Zone) { + continue + } + log.Printf("NBP: Converting FwdReq to LkUp (%v)", tuple) + + // Convert it to a LkUp and broadcast on the corresponding port + nbpkt.Function = nbp.FunctionLkUp + nbpRaw, err := nbpkt.Marshal() + if err != nil { + return fmt.Errorf("couldn't marshal LkUp: %v", err) + } + + // Inside AppleTalk SE, pp 8-20: + // "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 := outPort.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 := outPort.helloWorldThisIsMe(nbpkt.NBPID, tuple) + if err != nil || outDDP == nil { + return err + } + if err := rtr.Output(ctx, outDDP); err != nil { + return err + } + } + return nil +} + +// Returns an NBP LkUp-Reply for the router itself, with the address from this port. +func (port *EtherTalkPort) helloWorldThisIsMe(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 +223,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, }, }, } @@ -186,15 +237,25 @@ func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple * if err != nil { return nil, fmt.Errorf("couldn't marshal LkUp-Reply: %v", err) } + + // Inside AppleTalk SE, pp 7-16: + // "In BrRq, FwdReq, and LkUp packets, which carry only a single tuple, the + // address field contains the internet address of the requester, allowing + // the responder to address the LkUp-Reply datagram." + // Inside AppleTalk SE, pp 8-20: + // "Note: NBP is defined so that the router's NBP process does not + // participate in the NBP response process; the response is sent directly to + // the original requester through DDP. It is important that the original + // requester's field be obtained from the address field of the NBP tuple." return &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, + DstNet: tuple.Network, + DstNode: tuple.Node, + DstSocket: tuple.Socket, + 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..31b46af 100644 --- a/router/nbp_aurp.go +++ b/router/nbp_aurp.go @@ -17,14 +17,14 @@ package router import ( + "context" "fmt" - "log" "gitea.drjosh.dev/josh/jrouter/atalk/nbp" "github.com/sfiera/multitalk/pkg/ddp" ) -func (rtr *Router) HandleNBPInAURP(peer *AURPPeer, ddpkt *ddp.ExtPacket) error { +func (rtr *Router) HandleNBPFromAURP(ctx context.Context, ddpkt *ddp.ExtPacket) error { if ddpkt.Proto != ddp.ProtoNBP { return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto) } @@ -36,49 +36,5 @@ func (rtr *Router) HandleNBPInAURP(peer *AURPPeer, ddpkt *ddp.ExtPacket) error { // It's something else?? return fmt.Errorf("can't handle %v", nbpkt.Function) } - - if len(nbpkt.Tuples) < 1 { - return fmt.Errorf("no tuples in NBP packet") - } - 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) - } - - // 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 rtr.handleNBPFwdReq(ctx, ddpkt, nbpkt) } 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/peer_eth.go b/router/peer_eth.go index 046687e..4b181a2 100644 --- a/router/peer_eth.go +++ b/router/peer_eth.go @@ -19,29 +19,25 @@ package router import ( "context" - "github.com/google/gopacket/pcap" "github.com/sfiera/multitalk/pkg/ddp" - "github.com/sfiera/multitalk/pkg/ethernet" "github.com/sfiera/multitalk/pkg/ethertalk" ) -// EtherTalkPeer holds data needed to exchange routes and zones with another -// router on the EtherTalk network. +// EtherTalkPeer holds data needed to forward packets to another router on the +// EtherTalk network. type EtherTalkPeer struct { - PcapHandle *pcap.Handle - MyHWAddr ethernet.Addr - AARP *AARPMachine - PeerAddr ddp.Addr + Port *EtherTalkPort + PeerAddr ddp.Addr } // Forward forwards a DDP packet to the next router. func (p *EtherTalkPeer) Forward(ctx context.Context, pkt *ddp.ExtPacket) error { // TODO: AARP resolution can block - de, err := p.AARP.Resolve(ctx, p.PeerAddr) + de, err := p.Port.AARPMachine.Resolve(ctx, p.PeerAddr) if err != nil { return err } - outFrame, err := ethertalk.AppleTalk(p.MyHWAddr, *pkt) + outFrame, err := ethertalk.AppleTalk(p.Port.EthernetAddr, *pkt) if err != nil { return err } @@ -50,5 +46,5 @@ func (p *EtherTalkPeer) Forward(ctx context.Context, pkt *ddp.ExtPacket) error { if err != nil { return err } - return p.PcapHandle.WritePacketData(outFrameRaw) + return p.Port.PcapHandle.WritePacketData(outFrameRaw) } diff --git a/router/port.go b/router/port.go new file mode 100644 index 0000000..eb8aad6 --- /dev/null +++ b/router/port.go @@ -0,0 +1,193 @@ +/* + 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 { + Device string + EthernetAddr ethernet.Addr + NetStart ddp.Network + NetEnd ddp.Network + MyAddr ddp.Addr + DefaultZoneName string + AvailableZones []string + PcapHandle *pcap.Handle + AARPMachine *AARPMachine + 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 + } + port.MyAddr = 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 >= 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 + if err := port.HandleRTMP(ctx, ddpkt); err != nil { + log.Printf("RTMP: Couldn't handle: %v", err) + } + + 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..a399836 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) UpsertEtherTalkRoute(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..7295076 100644 --- a/router/router.go +++ b/router/router.go @@ -18,53 +18,56 @@ 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 + Ports []*EtherTalkPort } -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 increments the hop count, then outputs the packet in the direction +// of the destination. +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 + + return rtr.Output(ctx, ddpkt) } -func (rtr *Router) BroadcastEtherTalkDDP(pkt *ddp.ExtPacket) error { - return rtr.sendEtherTalkDDP(ethertalk.AppleTalkBroadcast, pkt) -} +// Output outputs the packet in the direction of the destination. +// (It does not check or adjust the hop count.) +func (rtr *Router) Output(ctx context.Context, ddpkt *ddp.ExtPacket) error { + 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..bdfb6b5 100644 --- a/router/rtmp.go +++ b/router/rtmp.go @@ -26,44 +26,131 @@ import ( "gitea.drjosh.dev/josh/jrouter/atalk/rtmp" "gitea.drjosh.dev/josh/jrouter/status" - "github.com/google/gopacket/pcap" - - "github.com/sfiera/multitalk/pkg/aarp" "github.com/sfiera/multitalk/pkg/ddp" - "github.com/sfiera/multitalk/pkg/ethernet" - "github.com/sfiera/multitalk/pkg/ethertalk" ) // RTMPMachine implements RTMP on an AppleTalk network attached to the router. -type RTMPMachine struct { - AARP *AARPMachine - Config *Config - PcapHandle *pcap.Handle - RoutingTable *RoutingTable +func (port *EtherTalkPort) HandleRTMP(ctx context.Context, pkt *ddp.ExtPacket) error { + switch pkt.Proto { + case ddp.ProtoRTMPReq: + // I can answer RTMP requests! + req, err := rtmp.UnmarshalRequestPacket(pkt.Data) + if err != nil { + return fmt.Errorf("unmarshal Request packet: %w", err) + } + + switch req.Function { + case rtmp.FunctionRequest: + // Respond with RTMP Response + respPkt := &rtmp.ResponsePacket{ + SenderAddr: port.MyAddr, + Extended: true, + RangeStart: port.NetStart, + RangeEnd: port.NetEnd, + } + respPktRaw, err := respPkt.Marshal() + if err != nil { + return fmt.Errorf("marshal RTMP Response packet: %w", err) + } + ddpPkt := &ddp.ExtPacket{ + ExtHeader: ddp.ExtHeader{ + Size: uint16(len(respPktRaw)) + atalk.DDPExtHeaderSize, + Cksum: 0, + DstNet: pkt.SrcNet, + DstNode: pkt.SrcNode, + DstSocket: 1, // the RTMP socket + SrcNet: port.MyAddr.Network, + SrcNode: port.MyAddr.Node, + SrcSocket: 1, // the RTMP socket + Proto: ddp.ProtoRTMPResp, + }, + Data: respPktRaw, + } + + if err := port.Router.Output(ctx, ddpPkt); err != nil { + return fmt.Errorf("send Response: %w", err) + } + + case rtmp.FunctionRDRSplitHorizon, rtmp.FunctionRDRComplete: + // Like the Data broadcast, but solicited by a request (RDR). + splitHorizon := req.Function == rtmp.FunctionRDRSplitHorizon + for _, dataPkt := range port.rtmpDataPackets(splitHorizon) { + dataPktRaw, err := dataPkt.Marshal() + if err != nil { + return fmt.Errorf("marshal RTMP Data packet: %w", err) + } + + ddpPkt := &ddp.ExtPacket{ + ExtHeader: ddp.ExtHeader{ + Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize, + Cksum: 0, + DstNet: pkt.SrcNet, + DstNode: pkt.SrcNode, + DstSocket: 1, // the RTMP socket + SrcNet: port.MyAddr.Network, + SrcNode: port.MyAddr.Node, + SrcSocket: 1, // the RTMP socket + Proto: ddp.ProtoRTMPResp, + }, + Data: dataPktRaw, + } + + if err := port.Router.Output(ctx, ddpPkt); err != nil { + return fmt.Errorf("send Data: %w", err) + } + } + + case rtmp.FunctionLoopProbe: + log.Print("RTMP: TODO: handle Loop Probes") + return nil + } + + case ddp.ProtoRTMPResp: + // It's a peer router on the AppleTalk network! + log.Print("RTMP: Got Response or Data") + dataPkt, err := rtmp.UnmarshalDataPacket(pkt.Data) + if err != nil { + log.Printf("RTMP: Couldn't unmarshal RTMP Data packet: %v", err) + break + } + peer := &EtherTalkPeer{ + Port: port, + PeerAddr: dataPkt.RouterAddr, + } + + for _, rt := range dataPkt.NetworkTuples { + if err := port.Router.RouteTable.UpsertEtherTalkRoute(peer, rt.Extended, rt.RangeStart, rt.RangeEnd, rt.Distance+1); err != nil { + log.Printf("RTMP: Couldn't upsert EtherTalk route: %v", err) + } + } + + default: + log.Printf("RTMP: invalid DDP type %d on socket 1", pkt.Proto) + } + + return nil } -// 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() +// RunRTMP makes periodic RTMP Data broadcasts on this port. +func (port *EtherTalkPort) RunRTMP(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() - if !ok { - return fmt.Errorf("AARP machine closed Assigned channel but Address is not valid") - } + <-port.AARPMachine.Assigned() setStatus("Initial RTMP Data broadcast") // Initial broadcast - if err := m.broadcastData(myAddr); err != nil { + if err := port.broadcastRTMPData(); err != nil { log.Printf("RTMP: Couldn't broadcast Data: %v", err) } - setStatus("Packet loop") + setStatus("Starting broadcast loop") bcastTicker := time.NewTicker(10 * time.Second) defer bcastTicker.Stop() @@ -74,140 +161,18 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket) return ctx.Err() case <-bcastTicker.C: - if err := m.broadcastData(myAddr); err != nil { - log.Printf("RTMP: Couldn't broadcast Data: %v", err) + setStatus("Broadcasting RTMP Data") + if err := port.broadcastRTMPData(); err != nil { + st := fmt.Sprintf("Couldn't broadcast Data: %v", err) + setStatus(st) + log.Print(st) } - - case pkt := <-incomingCh: - switch pkt.Proto { - case ddp.ProtoRTMPReq: - // I can answer RTMP requests! - req, err := rtmp.UnmarshalRequestPacket(pkt.Data) - if err != nil { - log.Printf("RTMP: Couldn't unmarshal Request packet: %v", err) - } - - // should be in the cache... - theirHWAddr, err := m.AARP.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 - } - - switch req.Function { - case rtmp.FunctionRequest: - // Respond with RTMP Response - respPkt := &rtmp.ResponsePacket{ - SenderAddr: myAddr.Proto, - Extended: true, - RangeStart: m.Config.EtherTalk.NetStart, - RangeEnd: m.Config.EtherTalk.NetEnd, - } - respPktRaw, err := respPkt.Marshal() - if err != nil { - log.Printf("RTMP: Couldn't marshal RTMP Response packet: %v", err) - continue - } - ddpPkt := &ddp.ExtPacket{ - ExtHeader: ddp.ExtHeader{ - Size: uint16(len(respPktRaw)) + atalk.DDPExtHeaderSize, - Cksum: 0, - DstNet: pkt.SrcNet, - DstNode: pkt.SrcNode, - DstSocket: 1, // the RTMP socket - SrcNet: myAddr.Proto.Network, - SrcNode: myAddr.Proto.Node, - SrcSocket: 1, // the RTMP socket - Proto: ddp.ProtoRTMPResp, - }, - Data: respPktRaw, - } - - if err := m.send(myAddr.Hardware, theirHWAddr, ddpPkt); err != nil { - log.Printf("RTMP: Couldn't send Data broadcast: %v", err) - } - - case rtmp.FunctionRDRSplitHorizon, rtmp.FunctionRDRComplete: - // Like the Data broadcast, but solicited by a request (RDR). - // TODO: handle split-horizon processing - for _, dataPkt := range m.dataPackets(myAddr.Proto) { - dataPktRaw, err := dataPkt.Marshal() - if err != nil { - log.Printf("RTMP: Couldn't marshal Data packet: %v", err) - break - } - - ddpPkt := &ddp.ExtPacket{ - ExtHeader: ddp.ExtHeader{ - Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize, - Cksum: 0, - DstNet: pkt.SrcNet, - DstNode: pkt.SrcNode, - DstSocket: 1, // the RTMP socket - SrcNet: myAddr.Proto.Network, - SrcNode: myAddr.Proto.Node, - SrcSocket: 1, // the RTMP socket - Proto: ddp.ProtoRTMPResp, - }, - Data: dataPktRaw, - } - - if err := m.send(myAddr.Hardware, theirHWAddr, ddpPkt); err != nil { - log.Printf("RTMP: Couldn't send Data response: %v", err) - break - } - } - - case rtmp.FunctionLoopProbe: - log.Print("RTMP: TODO: handle Loop Probes") - - } - - case ddp.ProtoRTMPResp: - // It's a peer router on the AppleTalk network! - log.Print("RTMP: Got Response or Data") - dataPkt, err := rtmp.UnmarshalDataPacket(pkt.Data) - if err != nil { - log.Printf("RTMP: Couldn't unmarshal RTMP Data packet: %v", err) - break - } - peer := &EtherTalkPeer{ - PcapHandle: m.PcapHandle, - MyHWAddr: m.AARP.myAddr.Hardware, - AARP: m.AARP, - PeerAddr: dataPkt.RouterAddr, - } - - for _, rt := range dataPkt.NetworkTuples { - if err := m.RoutingTable.UpsertEthRoute(peer, rt.Extended, rt.RangeStart, rt.RangeEnd, rt.Distance+1); err != nil { - log.Printf("RTMP: Couldn't upsert EtherTalk route: %v", err) - } - } - - default: - log.Printf("RTMP: invalid DDP type %d on socket 1", pkt.Proto) - } - } } } -func (m *RTMPMachine) send(src, dst ethernet.Addr, ddpPkt *ddp.ExtPacket) error { - ethFrame, err := ethertalk.AppleTalk(src, *ddpPkt) - if err != nil { - return err - } - ethFrame.Dst = dst - - ethFrameRaw, err := ethertalk.Marshal(*ethFrame) - if err != nil { - return err - } - return m.PcapHandle.WritePacketData(ethFrameRaw) -} - -func (m *RTMPMachine) broadcastData(myAddr aarp.AddrPair) error { - for _, dataPkt := range m.dataPackets(myAddr.Proto) { +func (port *EtherTalkPort) broadcastRTMPData() error { + for _, dataPkt := range port.rtmpDataPackets(true) { dataPktRaw, err := dataPkt.Marshal() if err != nil { return fmt.Errorf("marshal Data packet: %v", err) @@ -217,29 +182,40 @@ func (m *RTMPMachine) broadcastData(myAddr aarp.AddrPair) error { ExtHeader: ddp.ExtHeader{ Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize, Cksum: 0, - DstNet: 0, // this network - DstNode: 0xff, // broadcast packet - DstSocket: 1, // the RTMP socket - SrcNet: myAddr.Proto.Network, - SrcNode: myAddr.Proto.Node, + DstNet: 0x0000, // this network + DstNode: 0xff, // broadcast packet + DstSocket: 1, // the RTMP socket + SrcNet: port.MyAddr.Network, + SrcNode: port.MyAddr.Node, SrcSocket: 1, // the RTMP socket Proto: ddp.ProtoRTMPResp, }, Data: dataPktRaw, } - if err := m.send(myAddr.Hardware, ethertalk.AppleTalkBroadcast, ddpPkt); err != nil { + if err := port.Broadcast(ddpPkt); err != nil { return err } } return nil } -func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket { +func (port *EtherTalkPort) rtmpDataPackets(splitHorizon bool) []*rtmp.DataPacket { // Build up a slice of routing tuples. - routes := m.RoutingTable.ValidRoutes() + routes := port.Router.RouteTable.ValidRoutes() tuples := make([]rtmp.NetworkTuple, 0, len(routes)) for _, rt := range routes { + if rt.EtherTalkDirect == port { + // If the route is actually a direct connection to this port, + // don't include it. + // (It's manually set as the first tuple anyway.) + continue + } + if splitHorizon && rt.EtherTalkPeer != nil && rt.EtherTalkPeer.Port == port { + // If the route is through a peer accessible on this port, don't + // include it. + continue + } tuples = append(tuples, rtmp.NetworkTuple{ Extended: rt.Extended, RangeStart: rt.NetStart, @@ -253,8 +229,8 @@ func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket { // TODO: support non-extended local networks (LocalTalk) first := rtmp.NetworkTuple{ Extended: true, - RangeStart: m.Config.EtherTalk.NetStart, - RangeEnd: m.Config.EtherTalk.NetEnd, + RangeStart: port.NetStart, + RangeEnd: port.NetEnd, Distance: 0, } @@ -274,7 +250,7 @@ func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket { rem = rem[len(chunk)-1:] packets = append(packets, &rtmp.DataPacket{ - RouterAddr: myAddr, + RouterAddr: port.MyAddr, Extended: true, NetworkTuples: chunk, }) diff --git a/router/zip.go b/router/zip.go index ded821f..669e1f4 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 zoneValid if the zone name is available on this network. + zoneValid := slices.Contains(port.AvailableZones, zipkt.ZoneName) + + // The multicast address we return depends on the validity of the zone + // name. + var mcastAddr ethernet.Addr + if zoneValid { + mcastAddr = atalk.MulticastAddr(zipkt.ZoneName) + } else { + mcastAddr = atalk.MulticastAddr(port.DefaultZoneName) + } + + resp := &zip.GetNetInfoReplyPacket{ + ZoneInvalid: !zoneValid, + 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 !zoneValid { + 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()