Start implementing ZIP

This commit is contained in:
Josh Deprez 2024-04-13 17:36:55 +10:00
parent e20eb9be2d
commit 40867c23ea
Signed by: josh
SSH key fingerprint: SHA256:zZji7w1Ilh2RuUpbQcqkLPrqmRwpiCSycbF2EfKm6Kw
4 changed files with 278 additions and 1 deletions

107
atalk/zip/getnetinfo.go Normal file
View file

@ -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
}

36
atalk/zip/query_reply.go Normal file
View file

@ -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
}

66
atalk/zip/zip.go Normal file
View file

@ -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)})
}

70
main.go
View file

@ -34,6 +34,7 @@ import (
"gitea.drjosh.dev/josh/jrouter/atalk" "gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/aep" "gitea.drjosh.dev/josh/jrouter/atalk/aep"
"gitea.drjosh.dev/josh/jrouter/atalk/nbp" "gitea.drjosh.dev/josh/jrouter/atalk/nbp"
"gitea.drjosh.dev/josh/jrouter/atalk/zip"
"gitea.drjosh.dev/josh/jrouter/aurp" "gitea.drjosh.dev/josh/jrouter/aurp"
"github.com/google/gopacket/pcap" "github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ddp"
@ -267,9 +268,10 @@ func main() {
case 1: // The RTMP socket case 1: // The RTMP socket
rtmpCh <- ddpkt rtmpCh <- ddpkt
case 2: // The NIS (NBP socket) case 2: // The NIS (name information socket / NBP socket)
if ddpkt.Proto != ddp.ProtoNBP { if ddpkt.Proto != ddp.ProtoNBP {
log.Printf("NBP: invalid DDP type %d on socket 2", ddpkt.Proto) log.Printf("NBP: invalid DDP type %d on socket 2", ddpkt.Proto)
continue
} }
nbpkt, err := nbp.Unmarshal(ddpkt.Data) nbpkt, err := nbp.Unmarshal(ddpkt.Data)
@ -292,6 +294,72 @@ func main() {
log.Printf("AEP: Couldn't handle: %v", err) 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: default:
log.Printf("DDP: No handler for socket %d", ddpkt.DstSocket) log.Printf("DDP: No handler for socket %d", ddpkt.DstSocket)
} }