jrouter/zip.go
2024-04-15 13:06:52 +10:00

202 lines
5.6 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 main
import (
"fmt"
"log"
"gitea.drjosh.dev/josh/jrouter/atalk"
"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:
resp.Zones = []string{cfg.EtherTalk.ZoneName}
}
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: 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
}
var resp interface {
Marshal() ([]byte, error)
}
switch zipkt := zipkt.(type) {
case *zip.QueryPacket:
log.Printf("ZIP: Got Query for networks %v", zipkt.Networks)
// TODO: multiple packets
networks := zones.Query(zipkt.Networks)
resp = &zip.ReplyPacket{
Extended: false,
Networks: networks,
}
log.Printf("ZIP: Replying with non-extended Reply: %v", networks)
case *zip.GetNetInfoPacket:
log.Printf("ZIP: Got GetNetInfo for zone %q", zipkt.ZoneName)
// Only running a network with one zone for now.
gnir := &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,
}
log.Printf("ZIP: Replying with GetNetInfo-Reply: %+v", gnir)
resp = gnir
default:
return fmt.Errorf("TODO: handle type %T", zipkt)
}
if resp == nil {
return nil
}
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: myAddr.Proto.Network,
SrcNode: myAddr.Proto.Node,
SrcSocket: 6,
Proto: ddp.ProtoZIP,
},
Data: respRaw,
}
if ddpkt.DstNet == 0x0000 {
outDDP.DstNet = 0x0000
}
if ddpkt.DstNode == 0xFF {
outDDP.DstNode = 0xFF
}
outFrame, err := ethertalk.AppleTalk(myHWAddr, outDDP)
if err != nil {
return fmt.Errorf("couldn't create EtherTalk frame: %w", err)
}
// If it's a broadcast packet, broadcast it. Otherwise don't?
if outDDP.DstNet != 0x0000 || outDDP.DstNode != 0xFF {
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("invalid DDP type %d on socket 6", ddpkt.Proto)
}
}