diff --git a/aep.go b/aep.go new file mode 100644 index 0000000..f81d1a1 --- /dev/null +++ b/aep.go @@ -0,0 +1,66 @@ +/* + 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 main + +import ( + "fmt" + + "gitea.drjosh.dev/josh/jrouter/atalk/aep" + "github.com/google/gopacket/pcap" + "github.com/sfiera/multitalk/pkg/ddp" + "github.com/sfiera/multitalk/pkg/ethernet" + "github.com/sfiera/multitalk/pkg/ethertalk" +) + +func handleAEP(pcapHandle *pcap.Handle, src, dst ethernet.Addr, ddpkt *ddp.ExtPacket) error { + if ddpkt.Proto != ddp.ProtoAEP { + return fmt.Errorf("invalid DDP type %d on socket 4", ddpkt.Proto) + } + ep, err := aep.Unmarshal(ddpkt.Data) + if err != nil { + return err + } + switch ep.Function { + case aep.EchoReply: + // we didn't send a request? I don't think? + // we shouldn't be sending them from this socket + return fmt.Errorf("echo reply received at socket 4 why?") + + case aep.EchoRequest: + // Uno Reverso the packet + // "The client can send the Echo Request datagram through any socket + // the client has open, and the Echo Reply will come back to this socket." + ddpkt.DstNet, ddpkt.SrcNet = ddpkt.SrcNet, ddpkt.DstNet + ddpkt.DstNode, ddpkt.SrcNode = ddpkt.SrcNode, ddpkt.DstNode + ddpkt.DstSocket, ddpkt.SrcSocket = ddpkt.SrcSocket, ddpkt.DstSocket + ddpkt.Data[0] = byte(aep.EchoReply) + + ethFrame, err := ethertalk.AppleTalk(src, *ddpkt) + if err != nil { + return err + } + ethFrame.Dst = dst + ethFrameRaw, err := ethertalk.Marshal(*ethFrame) + if err != nil { + return err + } + return pcapHandle.WritePacketData(ethFrameRaw) + + default: + return fmt.Errorf("invalid AEP function %d", ep.Function) + } +} diff --git a/main.go b/main.go index 8178453..ddef2c8 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,6 @@ import ( "context" "errors" "flag" - "fmt" "io" "log" "math/rand/v2" @@ -32,12 +31,8 @@ import ( "time" "gitea.drjosh.dev/josh/jrouter/atalk" - "gitea.drjosh.dev/josh/jrouter/atalk/aep" - "gitea.drjosh.dev/josh/jrouter/atalk/nbp" - "gitea.drjosh.dev/josh/jrouter/atalk/zip" "gitea.drjosh.dev/josh/jrouter/aurp" "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" @@ -130,8 +125,10 @@ func main() { }() } - // ----------------------------- Routing table ---------------------------- + // -------------------------------- Tables -------------------------------- routing := NewRoutingTable() + zones := NewZoneTable() + zones.Upsert(cfg.EtherTalk.NetStart, cfg.EtherTalk.ZoneName, true) // ------------------------- Configured peer setup ------------------------ for _, peerStr := range cfg.Peers { @@ -156,6 +153,7 @@ func main() { raddr: raddr, recv: make(chan aurp.Packet, 1024), routingTable: routing, + zoneTable: zones, } aurp.Inc(&nextConnID) peers[udpAddrFromNet(raddr)] = peer @@ -285,7 +283,7 @@ func main() { } case 6: // The ZIS (zone information socket / ZIP socket) - if err := handleZIP(pcapHandle, ethFrame.Src, myHWAddr, myAddr, cfg, ddpkt); err != nil { + if err := handleZIP(pcapHandle, ethFrame.Src, myHWAddr, myAddr, cfg, zones, ddpkt); err != nil { log.Printf("ZIP: couldn't handle: %v", err) } @@ -414,6 +412,7 @@ func main() { raddr: raddr, recv: make(chan aurp.Packet, 1024), routingTable: routing, + zoneTable: zones, } aurp.Inc(&nextConnID) peers[ra] = pr @@ -431,262 +430,6 @@ func main() { } } -func handleNBP(pcapHandle *pcap.Handle, myHWAddr, srcHWAddr ethernet.Addr, myAddr aarp.AddrPair, cfg *config, ddpkt *ddp.ExtPacket) error { - if ddpkt.Proto != ddp.ProtoNBP { - return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto) - } - - nbpkt, err := nbp.Unmarshal(ddpkt.Data) - if err != nil { - return fmt.Errorf("invalid packet: %w", err) - } - - log.Printf("NBP: Got %v id %d with tuples %v", nbpkt.Function, nbpkt.NBPID, nbpkt.Tuples) - - switch nbpkt.Function { - case nbp.FunctionLkUp: - // when in AppleTalk, do as Apple Internet Router does... - tuple := nbpkt.Tuples[0] - if tuple.Object != "jrouter" && tuple.Object != "=" { - return nil - } - if tuple.Type != "AppleRouter" && tuple.Type != "=" { - return nil - } - if tuple.Zone != cfg.EtherTalk.ZoneName && tuple.Zone != "*" && tuple.Zone != "" { - return nil - } - respPkt := &nbp.Packet{ - Function: nbp.FunctionLkUpReply, - NBPID: nbpkt.NBPID, - Tuples: []nbp.Tuple{ - { - Network: myAddr.Proto.Network, - Node: myAddr.Proto.Node, - Socket: 253, - Enumerator: 0, - Object: "jrouter", - Type: "AppleRouter", - Zone: cfg.EtherTalk.ZoneName, - }, - }, - } - respRaw, err := respPkt.Marshal() - if err != nil { - return fmt.Errorf("couldn't marshal LkUp-Reply: %v", err) - } - ddpkt.DstNet = ddpkt.SrcNet - ddpkt.DstNode = ddpkt.SrcNode - ddpkt.DstSocket = ddpkt.SrcSocket - ddpkt.SrcNet = myAddr.Proto.Network - ddpkt.SrcNode = myAddr.Proto.Node - ddpkt.SrcSocket = 2 - ddpkt.Data = respRaw - outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) - if err != nil { - return err - } - outFrame.Dst = srcHWAddr - outFrameRaw, err := ethertalk.Marshal(*outFrame) - if err != nil { - return err - } - return pcapHandle.WritePacketData(outFrameRaw) - - case nbp.FunctionBrRq: - // There must be 1! - tuple := nbpkt.Tuples[0] - - if tuple.Zone != cfg.EtherTalk.ZoneName { - // TODO: Translate it into a FwdReq and route it to the - // routers with the appropriate zone(s). - return errors.New("TODO: BrRq-FwdReq translation") - } - - // 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." - // TODO: use zone-specific multicast - nbpkt.Function = nbp.FunctionLkUp - nbpRaw, err := nbpkt.Marshal() - if err != nil { - return fmt.Errorf("couldn't marshal LkUp: %v", err) - } - - ddpkt.DstNode = 0xFF // Broadcast node address within the dest network - ddpkt.Data = nbpRaw - - outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) - if err != nil { - return err - } - outFrameRaw, err := ethertalk.Marshal(*outFrame) - if err != nil { - return err - } - return pcapHandle.WritePacketData(outFrameRaw) - - default: - return fmt.Errorf("TODO: handle function %v", nbpkt.Function) - } -} - -func handleZIP(pcapHandle *pcap.Handle, srcHWAddr, myHWAddr ethernet.Addr, myAddr aarp.AddrPair, cfg *config, ddpkt *ddp.ExtPacket) error { - switch ddpkt.Proto { - case 3: // ATP - return errors.New("TODO implement ATP-based ZIP requests") - - case 6: // ZIP - zipkt, err := zip.UnmarshalPacket(ddpkt.Data) - if err != nil { - return err - } - switch zipkt := zipkt.(type) { - case *zip.GetNetInfoPacket: - // Only running a network with one zone for now. - resp := &zip.GetNetInfoReplyPacket{ - ZoneInvalid: zipkt.ZoneName != cfg.EtherTalk.ZoneName, - UseBroadcast: true, // TODO: add multicast addr computation - OnlyOneZone: true, - NetStart: cfg.EtherTalk.NetStart, - NetEnd: cfg.EtherTalk.NetEnd, - ZoneName: zipkt.ZoneName, // has to match request - MulticastAddr: ethertalk.AppleTalkBroadcast, - DefaultZoneName: cfg.EtherTalk.ZoneName, - } - respRaw, err := resp.Marshal() - if err != nil { - return fmt.Errorf("couldn't marshal GetNetInfoReplyPacket: %w", err) - } - - // TODO: fix - // "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." - ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket = 0x0000, 0xFF, ddpkt.SrcSocket - ddpkt.SrcNet = myAddr.Proto.Network - ddpkt.SrcNode = myAddr.Proto.Node - ddpkt.SrcSocket = 6 - ddpkt.Data = respRaw - outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) - if err != nil { - return fmt.Errorf("couldn't create EtherTalk frame: %w", err) - } - outFrame.Dst = srcHWAddr - outFrameRaw, err := ethertalk.Marshal(*outFrame) - if err != nil { - return fmt.Errorf("couldn't marshal EtherTalk frame: %w", err) - } - if err := pcapHandle.WritePacketData(outFrameRaw); err != nil { - return fmt.Errorf("couldn't write packet data: %w", err) - } - return nil - - default: - return fmt.Errorf("TODO: handle type %T", zipkt) - } - - default: - return fmt.Errorf("invalid DDP type %d on socket 6", ddpkt.Proto) - } -} - -func handleNBPInAURP(pcapHandle *pcap.Handle, myHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) error { - if ddpkt.Proto != ddp.ProtoNBP { - return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto) - } - nbpkt, err := nbp.Unmarshal(ddpkt.Data) - if err != nil { - return fmt.Errorf("invalid NBP packet: %v", err) - } - if nbpkt.Function != nbp.FunctionFwdReq { - // 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") - } - - log.Printf("NBP/DDP/AURP: Converting FwdReq to LkUp (%v)", nbpkt.Tuples[0]) - - // Convert it to a LkUp and broadcast on EtherTalk - // TODO: use zone-specific multicast - 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 - - outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) - if err != nil { - return err - } - // TODO: outFrame.Dst = zone-specific multicast address - outFrameRaw, err := ethertalk.Marshal(*outFrame) - if err != nil { - return err - } - return pcapHandle.WritePacketData(outFrameRaw) -} - -func handleAEP(pcapHandle *pcap.Handle, src, dst ethernet.Addr, ddpkt *ddp.ExtPacket) error { - if ddpkt.Proto != ddp.ProtoAEP { - return fmt.Errorf("invalid DDP type %d on socket 4", ddpkt.Proto) - } - ep, err := aep.Unmarshal(ddpkt.Data) - if err != nil { - return err - } - switch ep.Function { - case aep.EchoReply: - // we didn't send a request? I don't think? - // we shouldn't be sending them from this socket - return fmt.Errorf("echo reply received at socket 4 why?") - - case aep.EchoRequest: - // Uno Reverso the packet - // "The client can send the Echo Request datagram through any socket - // the client has open, and the Echo Reply will come back to this socket." - ddpkt.DstNet, ddpkt.SrcNet = ddpkt.SrcNet, ddpkt.DstNet - ddpkt.DstNode, ddpkt.SrcNode = ddpkt.SrcNode, ddpkt.DstNode - ddpkt.DstSocket, ddpkt.SrcSocket = ddpkt.SrcSocket, ddpkt.DstSocket - ddpkt.Data[0] = byte(aep.EchoReply) - - ethFrame, err := ethertalk.AppleTalk(src, *ddpkt) - if err != nil { - return err - } - ethFrame.Dst = dst - ethFrameRaw, err := ethertalk.Marshal(*ethFrame) - if err != nil { - return err - } - return pcapHandle.WritePacketData(ethFrameRaw) - - default: - return fmt.Errorf("invalid AEP function %d", ep.Function) - } -} - // Hashable net.UDPAddr type udpAddr struct { ipv4 [4]byte diff --git a/nbp.go b/nbp.go new file mode 100644 index 0000000..f36c2a0 --- /dev/null +++ b/nbp.go @@ -0,0 +1,135 @@ +/* + 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 main + +import ( + "errors" + "fmt" + "log" + + "gitea.drjosh.dev/josh/jrouter/atalk/nbp" + "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" +) + +func handleNBP(pcapHandle *pcap.Handle, myHWAddr, srcHWAddr ethernet.Addr, myAddr aarp.AddrPair, cfg *config, ddpkt *ddp.ExtPacket) error { + if ddpkt.Proto != ddp.ProtoNBP { + return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto) + } + + nbpkt, err := nbp.Unmarshal(ddpkt.Data) + if err != nil { + return fmt.Errorf("invalid packet: %w", err) + } + + log.Printf("NBP: Got %v id %d with tuples %v", nbpkt.Function, nbpkt.NBPID, nbpkt.Tuples) + + switch nbpkt.Function { + case nbp.FunctionLkUp: + // when in AppleTalk, do as Apple Internet Router does... + tuple := nbpkt.Tuples[0] + if tuple.Object != "jrouter" && tuple.Object != "=" { + return nil + } + if tuple.Type != "AppleRouter" && tuple.Type != "=" { + return nil + } + if tuple.Zone != cfg.EtherTalk.ZoneName && tuple.Zone != "*" && tuple.Zone != "" { + return nil + } + respPkt := &nbp.Packet{ + Function: nbp.FunctionLkUpReply, + NBPID: nbpkt.NBPID, + Tuples: []nbp.Tuple{ + { + Network: myAddr.Proto.Network, + Node: myAddr.Proto.Node, + Socket: 253, + Enumerator: 0, + Object: "jrouter", + Type: "AppleRouter", + Zone: cfg.EtherTalk.ZoneName, + }, + }, + } + respRaw, err := respPkt.Marshal() + if err != nil { + return fmt.Errorf("couldn't marshal LkUp-Reply: %v", err) + } + ddpkt.DstNet = ddpkt.SrcNet + ddpkt.DstNode = ddpkt.SrcNode + ddpkt.DstSocket = ddpkt.SrcSocket + ddpkt.SrcNet = myAddr.Proto.Network + ddpkt.SrcNode = myAddr.Proto.Node + ddpkt.SrcSocket = 2 + ddpkt.Data = respRaw + outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) + if err != nil { + return err + } + outFrame.Dst = srcHWAddr + outFrameRaw, err := ethertalk.Marshal(*outFrame) + if err != nil { + return err + } + return pcapHandle.WritePacketData(outFrameRaw) + + case nbp.FunctionBrRq: + // There must be 1! + tuple := nbpkt.Tuples[0] + + if tuple.Zone != cfg.EtherTalk.ZoneName { + // TODO: Translate it into a FwdReq and route it to the + // routers with the appropriate zone(s). + return errors.New("TODO: BrRq-FwdReq translation") + } + + // 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." + // TODO: use zone-specific multicast + nbpkt.Function = nbp.FunctionLkUp + nbpRaw, err := nbpkt.Marshal() + if err != nil { + return fmt.Errorf("couldn't marshal LkUp: %v", err) + } + + ddpkt.DstNode = 0xFF // Broadcast node address within the dest network + ddpkt.Data = nbpRaw + + outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) + if err != nil { + return err + } + outFrameRaw, err := ethertalk.Marshal(*outFrame) + if err != nil { + return err + } + return pcapHandle.WritePacketData(outFrameRaw) + + default: + return fmt.Errorf("TODO: handle function %v", nbpkt.Function) + } +} diff --git a/nbp_aurp.go b/nbp_aurp.go new file mode 100644 index 0000000..a566dda --- /dev/null +++ b/nbp_aurp.go @@ -0,0 +1,75 @@ +/* + 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 main + +import ( + "fmt" + "log" + + "gitea.drjosh.dev/josh/jrouter/atalk/nbp" + "github.com/google/gopacket/pcap" + "github.com/sfiera/multitalk/pkg/ddp" + "github.com/sfiera/multitalk/pkg/ethernet" + "github.com/sfiera/multitalk/pkg/ethertalk" +) + +func handleNBPInAURP(pcapHandle *pcap.Handle, myHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) error { + if ddpkt.Proto != ddp.ProtoNBP { + return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto) + } + nbpkt, err := nbp.Unmarshal(ddpkt.Data) + if err != nil { + return fmt.Errorf("invalid NBP packet: %v", err) + } + if nbpkt.Function != nbp.FunctionFwdReq { + // 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") + } + + log.Printf("NBP/DDP/AURP: Converting FwdReq to LkUp (%v)", nbpkt.Tuples[0]) + + // Convert it to a LkUp and broadcast on EtherTalk + // TODO: use zone-specific multicast + 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 + + outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) + if err != nil { + return err + } + // TODO: outFrame.Dst = zone-specific multicast address + outFrameRaw, err := ethertalk.Marshal(*outFrame) + if err != nil { + return err + } + return pcapHandle.WritePacketData(outFrameRaw) +} diff --git a/peer.go b/peer.go index 3eb23ca..3855495 100644 --- a/peer.go +++ b/peer.go @@ -83,6 +83,7 @@ type peer struct { recv chan aurp.Packet routingTable *RoutingTable + zoneTable *ZoneTable } // send encodes and sends pkt to the remote host. @@ -367,8 +368,10 @@ func (p *peer) handle(ctx context.Context) error { } case *aurp.ZIRspPacket: - // TODO: Integrate info into zone table log.Printf("Learned about these zones: %v", pkt.Zones) + for _, zt := range pkt.Zones { + p.zoneTable.Upsert(ddp.Network(zt.Network), zt.Name, false) + } case *aurp.GDZLReqPacket: if _, err := p.send(p.tr.NewGDZLRspPacket(-1, nil)); err != nil { diff --git a/zip.go b/zip.go new file mode 100644 index 0000000..a970ef3 --- /dev/null +++ b/zip.go @@ -0,0 +1,156 @@ +/* + 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 main + +import ( + "fmt" + + "gitea.drjosh.dev/josh/jrouter/atalk/atp" + "gitea.drjosh.dev/josh/jrouter/atalk/zip" + "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" +) + +func handleZIP(pcapHandle *pcap.Handle, srcHWAddr, myHWAddr ethernet.Addr, myAddr aarp.AddrPair, cfg *config, zones *ZoneTable, ddpkt *ddp.ExtPacket) error { + switch ddpkt.Proto { + case 3: // ATP + 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 + } + // TODO: handle this in a more transactiony way + resp := &zip.GetZonesReplyPacket{ + TID: gzl.TID, + LastFlag: true, // TODO: support multiple response packets + } + switch gzl.Function { + case zip.FunctionGetZoneList: + resp.Zones = zones.AllNames() + + case zip.FunctionGetLocalZones: + resp.Zones = zones.LocalNames() + + case zip.FunctionGetMyZone: + return fmt.Errorf("TODO: support GetMyZone?") + } + + respATP, err := resp.MarshalTResp() + if err != nil { + return err + } + ddpBody, err := respATP.Marshal() + if err != nil { + return err + } + respDDP := ddp.ExtPacket{ + ExtHeader: ddp.ExtHeader{ + DstNet: ddpkt.SrcNet, + DstNode: ddpkt.SrcNode, + DstSocket: ddpkt.SrcSocket, + SrcNet: myAddr.Proto.Network, + SrcNode: myAddr.Proto.Node, + SrcSocket: 6, + Proto: ddp.ProtoATP, + }, + Data: ddpBody, + } + outFrame, err := ethertalk.AppleTalk(myHWAddr, respDDP) + if err != nil { + return err + } + outFrame.Dst = srcHWAddr + outFrameRaw, err := ethertalk.Marshal(*outFrame) + if err != nil { + return err + } + return pcapHandle.WritePacketData(outFrameRaw) + + case *atp.TResp: + return fmt.Errorf("TODO: support handling ZIP ATP replies?") + + default: + return fmt.Errorf("unsupported ATP packet type %T for ZIP", atpkt) + } + + case 6: // ZIP + zipkt, err := zip.UnmarshalPacket(ddpkt.Data) + if err != nil { + return err + } + switch zipkt := zipkt.(type) { + case *zip.GetNetInfoPacket: + // Only running a network with one zone for now. + resp := &zip.GetNetInfoReplyPacket{ + ZoneInvalid: zipkt.ZoneName != cfg.EtherTalk.ZoneName, + UseBroadcast: true, // TODO: add multicast addr computation + OnlyOneZone: true, + NetStart: cfg.EtherTalk.NetStart, + NetEnd: cfg.EtherTalk.NetEnd, + ZoneName: zipkt.ZoneName, // has to match request + MulticastAddr: ethertalk.AppleTalkBroadcast, + DefaultZoneName: cfg.EtherTalk.ZoneName, + } + respRaw, err := resp.Marshal() + if err != nil { + return fmt.Errorf("couldn't marshal GetNetInfoReplyPacket: %w", err) + } + + // TODO: fix + // "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." + ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket = 0x0000, 0xFF, ddpkt.SrcSocket + ddpkt.SrcNet = myAddr.Proto.Network + ddpkt.SrcNode = myAddr.Proto.Node + ddpkt.SrcSocket = 6 + ddpkt.Data = respRaw + outFrame, err := ethertalk.AppleTalk(myHWAddr, *ddpkt) + if err != nil { + return fmt.Errorf("couldn't create EtherTalk frame: %w", err) + } + outFrame.Dst = srcHWAddr + outFrameRaw, err := ethertalk.Marshal(*outFrame) + if err != nil { + return fmt.Errorf("couldn't marshal EtherTalk frame: %w", err) + } + if err := pcapHandle.WritePacketData(outFrameRaw); err != nil { + return fmt.Errorf("couldn't write packet data: %w", err) + } + return nil + + default: + return fmt.Errorf("TODO: handle type %T", zipkt) + } + + default: + return fmt.Errorf("invalid DDP type %d on socket 6", ddpkt.Proto) + } +} diff --git a/zones.go b/zones.go new file mode 100644 index 0000000..306fa9c --- /dev/null +++ b/zones.go @@ -0,0 +1,112 @@ +/* + 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 main + +import ( + "sort" + "sync" + "time" + + "github.com/sfiera/multitalk/pkg/ddp" +) + +const maxZoneAge = 10 * time.Minute // TODO: confirm + +type Zone struct { + Network ddp.Network + Name string + Local bool + LastSeen time.Time +} + +type zoneKey struct { + network ddp.Network + name string +} + +type ZoneTable struct { + mu sync.Mutex + zones map[zoneKey]*Zone +} + +func NewZoneTable() *ZoneTable { + return &ZoneTable{ + zones: make(map[zoneKey]*Zone), + } +} + +func (zt *ZoneTable) Upsert(network ddp.Network, name string, local bool) { + zt.mu.Lock() + defer zt.mu.Unlock() + key := zoneKey{network, name} + z := zt.zones[key] + if z != nil { + z.Local = local + z.LastSeen = time.Now() + return + } + zt.zones[key] = &Zone{ + Network: network, + Name: name, + Local: local, + LastSeen: time.Now(), + } +} + +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) + + } + zt.mu.Unlock() + + sort.Strings(zs) + return zs +} + +func (zt *ZoneTable) AllNames() []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 _, s := seen[z.Name]; s { + continue + } + seen[z.Name] = struct{}{} + zs = append(zs, z.Name) + } + zt.mu.Unlock() + + sort.Strings(zs) + return zs +}