319 lines
8.8 KiB
Go
319 lines
8.8 KiB
Go
/*
|
|
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"
|
|
"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"
|
|
)
|
|
|
|
func (port *EtherTalkPort) HandleZIP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
|
|
switch ddpkt.Proto {
|
|
case ddp.ProtoATP:
|
|
return port.handleZIPATP(ctx, ddpkt)
|
|
|
|
case ddp.ProtoZIP:
|
|
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.SendEtherTalkDDP(ctx, outDDP)
|
|
}
|
|
|
|
// Inside AppleTalk SE, pp 8-11:
|
|
//
|
|
// "Replies (but not Extended Replies) can contain any number of
|
|
// zones lists, as long as the zones list for each network is
|
|
// entirely contained in the Reply packet."
|
|
//
|
|
// and
|
|
//
|
|
// "The zones list for a given network must be contiguous in the
|
|
// packet, with each zone name in that list preceded by the first
|
|
// network number in the range of the requested network."
|
|
size := 2
|
|
for _, zl := range networks {
|
|
for _, z := range zl {
|
|
size += 3 + len(z) // Network number, length byte, string
|
|
}
|
|
}
|
|
|
|
if size <= atalk.DDPMaxDataSize {
|
|
// Send one non-extended reply packet with all the data
|
|
log.Printf("ZIP: Replying with non-extended Reply: %v", networks)
|
|
return sendReply(&zip.ReplyPacket{
|
|
Extended: false,
|
|
// "Replies contain the number of zones lists indicated in
|
|
// the Reply header."
|
|
NetworkCount: uint8(len(networks)),
|
|
Networks: networks,
|
|
})
|
|
}
|
|
|
|
// Send Extended Reply packets, 1 or more for each network
|
|
//
|
|
// "Extended Replies can contain only one zones list."
|
|
for nn, zl := range networks {
|
|
rem := zl // rem: remaining zone names to send for this network
|
|
for len(rem) > 0 {
|
|
size := 2
|
|
var chunk []string // chunk: zone names to send now
|
|
for _, z := range rem {
|
|
size += 3 + len(z)
|
|
if size > atalk.DDPMaxDataSize {
|
|
break
|
|
}
|
|
chunk = append(chunk, z)
|
|
}
|
|
rem = rem[len(chunk):]
|
|
|
|
nets := map[ddp.Network][]string{
|
|
nn: chunk,
|
|
}
|
|
log.Printf("ZIP: Replying with Extended Reply: %v", nets)
|
|
err := sendReply(&zip.ReplyPacket{
|
|
Extended: true,
|
|
// "The network count in the header indicates, not the
|
|
// number of zones names in the packet, but the number
|
|
// of zone names in the entire zones list for the
|
|
// requested network, which may span more than one
|
|
// packet."
|
|
NetworkCount: uint8(len(zl)),
|
|
Networks: nets,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (port *EtherTalkPort) handleZIPGetNetInfo(ctx context.Context, ddpkt *ddp.ExtPacket, zipkt *zip.GetNetInfoPacket) error {
|
|
log.Printf("ZIP: Got GetNetInfo for zone %q", zipkt.ZoneName)
|
|
|
|
// The request is valid if the zone name is available on this network.
|
|
valid := slices.Contains(port.AvailableZones, zipkt.ZoneName)
|
|
|
|
// The multicast address we return depends on the validity of the zone
|
|
// name.
|
|
var mcastAddr ethernet.Addr
|
|
if valid {
|
|
mcastAddr = atalk.MulticastAddr(zipkt.ZoneName)
|
|
} else {
|
|
mcastAddr = atalk.MulticastAddr(port.DefaultZoneName)
|
|
}
|
|
|
|
resp := &zip.GetNetInfoReplyPacket{
|
|
ZoneInvalid: valid,
|
|
UseBroadcast: false,
|
|
OnlyOneZone: len(port.AvailableZones) == 1,
|
|
NetStart: port.NetStart,
|
|
NetEnd: port.NetEnd,
|
|
ZoneName: zipkt.ZoneName, // has to match request
|
|
MulticastAddr: mcastAddr,
|
|
}
|
|
// The default zone name is only returned if the requested zone name is
|
|
// invalid.
|
|
if !valid {
|
|
resp.DefaultZoneName = port.DefaultZoneName
|
|
}
|
|
log.Printf("ZIP: Replying with GetNetInfo-Reply: %+v", resp)
|
|
|
|
respRaw, err := resp.Marshal()
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't marshal %T: %w", resp, err)
|
|
}
|
|
|
|
// "In cases where a node's provisional address is
|
|
// invalid, routers will not be able to respond to
|
|
// the node in a directed manner. An address is
|
|
// invalid if the network number is neither in the
|
|
// startup range nor in the network number range
|
|
// assigned to the node's network. In these cases,
|
|
// if the request was sent via a broadcast, the
|
|
// routers should respond with a broadcast."
|
|
outDDP := &ddp.ExtPacket{
|
|
ExtHeader: ddp.ExtHeader{
|
|
Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize,
|
|
Cksum: 0,
|
|
DstNet: ddpkt.SrcNet,
|
|
DstNode: ddpkt.SrcNode,
|
|
DstSocket: ddpkt.SrcSocket,
|
|
SrcNet: port.MyAddr.Network,
|
|
SrcNode: port.MyAddr.Node,
|
|
SrcSocket: 6,
|
|
Proto: ddp.ProtoZIP,
|
|
},
|
|
Data: respRaw,
|
|
}
|
|
// If it arrived as a broadcast, send the reply as a broadcast.
|
|
if ddpkt.DstNet == 0x0000 {
|
|
outDDP.DstNet = 0x0000
|
|
}
|
|
if ddpkt.DstNode == 0xFF {
|
|
outDDP.DstNode = 0xFF
|
|
}
|
|
// Note: AARP can block
|
|
return port.SendEtherTalkDDP(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.SendEtherTalkDDP(ctx, respDDP)
|
|
}
|