diff --git a/atalk/zip/getnetinfo.go b/atalk/zip/getnetinfo.go new file mode 100644 index 0000000..b320c60 --- /dev/null +++ b/atalk/zip/getnetinfo.go @@ -0,0 +1,107 @@ +/* + 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 zip + +import ( + "bytes" + "fmt" + + "github.com/sfiera/multitalk/pkg/ddp" + "github.com/sfiera/multitalk/pkg/ethernet" +) + +type GetNetInfoPacket struct { + // Destination socket = 6 + // DDP type = 6 + // --- + // ZIP command = 5 + // Flags = 0 (reserved) + // Four more bytes of 0 (reserved) + // Zone name length (1 byte) + ZoneName string +} + +func UnmarshalGetNetInfoPacket(data []byte) (*GetNetInfoPacket, error) { + if len(data) < 7 { + return nil, fmt.Errorf("insufficient input length %d for GetNetInfo packet", len(data)) + } + if data[0] != FunctionGetNetInfo { + return nil, fmt.Errorf("not a GetNetInfo packet (ZIP command %d != %d)", data[0], FunctionGetNetInfo) + } + slen := data[6] + data = data[7:] + if len(data) != int(slen) { + return nil, fmt.Errorf("wrong remaining input length %d for length=%d-prefixed string", len(data), slen) + } + return &GetNetInfoPacket{ + ZoneName: string(data), + }, nil +} + +type GetNetInfoReplyPacket struct { + // Source socket = 6 + // DDP type = 6 + // --- + // ZIP command = 6 + ZoneInvalid bool // 0x80 + UseBroadcast bool // 0x40 + OnlyOneZone bool // 0x20 + // Remainder of flags reserved + NetStart ddp.Network + NetEnd ddp.Network + // Zone name length (1 byte) + ZoneName string + // Multicast address length (1 byte) + MulticastAddr ethernet.Addr + // Only if ZoneInvalid flag is set: + // Default zone length (1 byte) + DefaultZoneName string +} + +func (p *GetNetInfoReplyPacket) Marshal() ([]byte, error) { + if len(p.ZoneName) > 32 { + return nil, fmt.Errorf("zone name too long [%d > 32]", len(p.ZoneName)) + } + if len(p.DefaultZoneName) > 32 { + return nil, fmt.Errorf("default zone name too long [%d > 32]", len(p.DefaultZoneName)) + } + + b := bytes.NewBuffer(nil) + b.WriteByte(FunctionGetNetInfoReply) + var flags byte + if p.ZoneInvalid { + flags |= 0x80 + } + if p.UseBroadcast { + flags |= 0x40 + } + if p.OnlyOneZone { + flags |= 0x20 + } + b.WriteByte(flags) + write16(b, p.NetStart) + write16(b, p.NetEnd) + b.WriteByte(byte(len(p.ZoneName))) + b.WriteString(p.ZoneName) + b.WriteByte(6) + b.Write(p.MulticastAddr[:]) + if p.ZoneInvalid { + b.WriteByte(byte(len(p.DefaultZoneName))) + b.WriteString(p.DefaultZoneName) + } + return b.Bytes(), nil +} diff --git a/atalk/zip/query_reply.go b/atalk/zip/query_reply.go new file mode 100644 index 0000000..fea06c5 --- /dev/null +++ b/atalk/zip/query_reply.go @@ -0,0 +1,36 @@ +/* + 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 zip + +import "github.com/sfiera/multitalk/pkg/ddp" + +type QueryPacket struct { + Function Function // 1 + // NetworkCount uint8 + Networks []ddp.Network +} + +type ReplyPacket struct { + Function Function // 2 or 8 + // NetworkCount uint8 + Tuples []ZoneTuple +} + +type ZoneTuple struct { + Network ddp.Network + ZoneName string +} diff --git a/atalk/zip/zip.go b/atalk/zip/zip.go new file mode 100644 index 0000000..9e4373d --- /dev/null +++ b/atalk/zip/zip.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 zip + +import ( + "bytes" + "fmt" +) + +type Function uint8 + +const ( + FunctionQuery = 1 + FunctionReply = 2 + FunctionGetNetInfo = 5 + FunctionGetNetInfoReply = 6 + FunctionNotify = 7 + FunctionExtendedReply = 8 +) + +// Non-ATP packets only +func UnmarshalPacket(data []byte) (any, error) { + if len(data) < 1 { + return nil, fmt.Errorf("insufficient input length %d for any ZIP packet", len(data)) + } + switch data[0] { + case FunctionQuery: + return nil, fmt.Errorf("ZIP Query unmarshaling unimplemented") + + case FunctionReply: + return nil, fmt.Errorf("ZIP Reply unmarshaling unimplemented") + + case FunctionExtendedReply: + return nil, fmt.Errorf("ZIP Extended Reply unmarshaling unimplemented") + + case FunctionGetNetInfo: + return UnmarshalGetNetInfoPacket(data) + + case FunctionGetNetInfoReply: + return nil, fmt.Errorf("ZIP GetNetInfo Reply unmarshaling unimplemented") + + case FunctionNotify: + return nil, fmt.Errorf("ZIP Notify unmarshaling unimplemented") + + default: + return nil, fmt.Errorf("unknown ZIP function %d", data[0]) + } +} + +func write16[I ~uint16](b *bytes.Buffer, n I) { + b.Write([]byte{byte(n >> 8), byte(n & 0xff)}) +} diff --git a/main.go b/main.go index 4e9c5e2..9d77e21 100644 --- a/main.go +++ b/main.go @@ -34,6 +34,7 @@ import ( "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/ddp" @@ -267,9 +268,10 @@ func main() { case 1: // The RTMP socket rtmpCh <- ddpkt - case 2: // The NIS (NBP socket) + case 2: // The NIS (name information socket / NBP socket) if ddpkt.Proto != ddp.ProtoNBP { log.Printf("NBP: invalid DDP type %d on socket 2", ddpkt.Proto) + continue } nbpkt, err := nbp.Unmarshal(ddpkt.Data) @@ -292,6 +294,72 @@ func main() { log.Printf("AEP: Couldn't handle: %v", err) } + case 6: // The ZIS (zone information socket / ZIP socket) + switch ddpkt.Proto { + case 3: // ATP + log.Print("ZIP: TODO implement ATP-based ZIP requests") + continue + + case 6: // ZIP + zipkt, err := zip.UnmarshalPacket(ddpkt.Data) + if err != nil { + log.Printf("ZIP: invalid packet: %v", err) + continue + } + 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 { + log.Printf("ZIP: couldn't marshal GetNetInfoReplyPacket: %v", err) + continue + } + + // 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 { + log.Printf("ZIP: couldn't create EtherTalk frame: %v", err) + continue + } + outFrame.Dst = ethFrame.Src + outFrameRaw, err := ethertalk.Marshal(*outFrame) + if err != nil { + log.Printf("ZIP: couldn't marshal EtherTalk frame: %v", err) + continue + } + if err := pcapHandle.WritePacketData(outFrameRaw); err != nil { + log.Printf("ZIP: couldn't write packet data: %v", err) + } + } + + default: + log.Printf("ZIP: invalid DDP type %d on socket 6", ddpkt.Proto) + continue + } + default: log.Printf("DDP: No handler for socket %d", ddpkt.DstSocket) }