diff --git a/atalk/nbp/nbp.go b/atalk/nbp/nbp.go
index 423533d..c2a948c 100644
--- a/atalk/nbp/nbp.go
+++ b/atalk/nbp/nbp.go
@@ -101,7 +101,7 @@ func Unmarshal(data []byte) (*Packet, error) {
t := Tuple{
Network: ddp.Network(binary.BigEndian.Uint16(data[:2])),
Node: ddp.Node(data[2]),
- Socket: data[3],
+ Socket: ddp.Socket(data[3]),
Enumerator: data[4],
}
data = data[5:]
@@ -128,7 +128,7 @@ func Unmarshal(data []byte) (*Packet, error) {
type Tuple struct {
Network ddp.Network
Node ddp.Node
- Socket uint8
+ Socket ddp.Socket
Enumerator uint8
Object string // length-prefixed
Type string // length-prefixed
@@ -147,7 +147,7 @@ func (t *Tuple) writeTo(b *bytes.Buffer) error {
}
write16(b, t.Network)
b.WriteByte(byte(t.Node))
- b.WriteByte(t.Socket)
+ b.WriteByte(byte(t.Socket))
b.WriteByte(t.Enumerator)
b.WriteByte(byte(len(t.Object)))
b.WriteString(t.Object)
diff --git a/main.go b/main.go
index b17fac3..59932e7 100644
--- a/main.go
+++ b/main.go
@@ -23,7 +23,6 @@ import (
"errors"
"flag"
"fmt"
- "io"
"log"
"math/rand/v2"
"net"
@@ -44,7 +43,6 @@ import (
"github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
- "github.com/sfiera/multitalk/pkg/ethertalk"
)
const routingTableTemplate = `
@@ -63,7 +61,17 @@ const routingTableTemplate = `
{{if $route.Extended}}✅{{else}}❌{{end}} |
{{$route.Distance}} |
{{$route.LastSeenAgo}} |
- {{if $route.AURPPeer}}{{$route.AURPPeer.RemoteAddr}}{{else if $route.EtherTalkPeer}}{{$route.EtherTalkPeer.PeerAddr.Network}}.{{$route.EtherTalkPeer.PeerAddr.Node}}{{else}}-{{end}} |
+
+ {{- with $route.AURPPeer -}}
+ {{.RemoteAddr}}
+ {{- end -}}
+ {{- with $route.EtherTalkPeer -}}
+ {{.Port.Device}} {{.PeerAddr.Network}}.{{.PeerAddr.Node}}
+ {{- end -}}
+ {{- with $route.EtherTalkDirect -}}
+ {{.Device}} {{.NetStart}}-{{.NetEnd}}
+ {{- end -}}
+ |
{{end}}
@@ -75,7 +83,7 @@ const zoneTableTemplate = `
Network |
Name |
- Local |
+ Local Port |
Last seen |
@@ -83,7 +91,7 @@ const zoneTableTemplate = `
{{$zone.Network}} |
{{$zone.Name}} |
- {{if $zone.Local}}✅{{else}}❌{{end}} |
+ {{with $zone.LocalPort}}{{.Device}}{{else}}-{{end}} |
{{$zone.LastSeenAgo}} |
{{end}}
@@ -159,10 +167,10 @@ func main() {
ln, err := net.ListenUDP("udp4", &net.UDPAddr{Port: int(cfg.ListenPort)})
if err != nil {
- log.Fatalf("Couldn't listen on udp4:387: %v", err)
+ log.Fatalf("AURP: Couldn't listen on udp4:387: %v", err)
}
defer ln.Close()
- log.Printf("Listening on %v", ln.LocalAddr())
+ log.Printf("AURP: Listening on %v", ln.LocalAddr())
log.Println("Press ^C or send SIGINT to stop the router gracefully")
cctx, cancel := context.WithCancel(context.Background())
@@ -203,7 +211,7 @@ func main() {
defer pcapHandle.Close()
// -------------------------------- Tables --------------------------------
- routes := router.NewRoutingTable()
+ routes := router.NewRouteTable()
status.AddItem(ctx, "Routing table", routingTableTemplate, func(context.Context) (any, error) {
rs := routes.Dump()
slices.SortFunc(rs, func(ra, rb router.Route) int {
@@ -213,7 +221,6 @@ func main() {
})
zones := router.NewZoneTable()
- zones.Upsert(cfg.EtherTalk.NetStart, cfg.EtherTalk.ZoneName, true)
status.AddItem(ctx, "Zone table", zoneTableTemplate, func(context.Context) (any, error) {
zs := zones.Dump()
slices.SortFunc(zs, func(za, zb router.Zone) int {
@@ -331,174 +338,46 @@ func main() {
// --------------------------------- AARP ---------------------------------
aarpMachine := router.NewAARPMachine(cfg, pcapHandle, myHWAddr)
- aarpCh := make(chan *ethertalk.Packet, 1024)
- go aarpMachine.Run(ctx, aarpCh)
-
- // --------------------------------- RTMP ---------------------------------
- rtmpMachine := &router.RTMPMachine{
- AARP: aarpMachine,
- Config: cfg,
- PcapHandle: pcapHandle,
- RoutingTable: routes,
- }
- rtmpCh := make(chan *ddp.ExtPacket, 1024)
- go rtmpMachine.Run(ctx, rtmpCh)
+ go aarpMachine.Run(ctx)
// -------------------------------- Router --------------------------------
rooter := &router.Router{
Config: cfg,
- PcapHandle: pcapHandle,
- MyHWAddr: myHWAddr,
- // MyDDPAddr: ...,
- AARPMachine: aarpMachine,
- RouteTable: routes,
- ZoneTable: zones,
+ RouteTable: routes,
+ ZoneTable: zones,
}
+ etherTalkPort := &router.EtherTalkPort{
+ Device: cfg.EtherTalk.Device,
+ EthernetAddr: myHWAddr,
+ NetStart: cfg.EtherTalk.NetStart,
+ NetEnd: cfg.EtherTalk.NetEnd,
+ DefaultZoneName: cfg.EtherTalk.ZoneName,
+ AvailableZones: []string{cfg.EtherTalk.ZoneName},
+ PcapHandle: pcapHandle,
+ AARPMachine: aarpMachine,
+ Router: rooter,
+ }
+ rooter.Ports = append(rooter.Ports, etherTalkPort)
+ routes.InsertEtherTalkDirect(etherTalkPort)
+ for _, az := range etherTalkPort.AvailableZones {
+ zones.Upsert(etherTalkPort.NetStart, az, etherTalkPort)
+ }
+
+ // --------------------------------- RTMP ---------------------------------
+ go etherTalkPort.RunRTMP(ctx)
+
// ---------------------- Raw AppleTalk/AARP inbound ----------------------
wg.Add(1)
go func() {
defer wg.Done()
- ctx, setStatus, done := status.AddSimpleItem(ctx, "EtherTalk inbound")
- defer done()
+ ctx, setStatus, _ := status.AddSimpleItem(ctx, "EtherTalk inbound")
+ defer setStatus("EtherTalk Serve goroutine exited!")
setStatus(fmt.Sprintf("Listening on %s", cfg.EtherTalk.Device))
- for {
- if ctx.Err() != nil {
- return
- }
-
- rawPkt, _, err := pcapHandle.ReadPacketData()
- if errors.Is(err, pcap.NextErrorTimeoutExpired) {
- continue
- }
- if errors.Is(err, io.EOF) || errors.Is(err, pcap.NextErrorNoMorePackets) {
- return
- }
- if err != nil {
- log.Printf("Couldn't read AppleTalk / AARP packet data: %v", err)
- return
- }
-
- ethFrame := new(ethertalk.Packet)
- if err := ethertalk.Unmarshal(rawPkt, ethFrame); err != nil {
- log.Printf("Couldn't unmarshal EtherTalk frame: %v", err)
- continue
- }
-
- // Ignore if sent by me
- if ethFrame.Src == myHWAddr {
- continue
- }
-
- switch ethFrame.SNAPProto {
- case ethertalk.AARPProto:
- // log.Print("Got an AARP frame")
- aarpCh <- ethFrame
-
- case ethertalk.AppleTalkProto:
- // log.Print("Got an AppleTalk frame")
- ddpkt := new(ddp.ExtPacket)
- if err := ddp.ExtUnmarshal(ethFrame.Payload, ddpkt); err != nil {
- log.Printf("Couldn't unmarshal DDP packet: %v", err)
- continue
- }
- log.Printf("DDP: src (%d.%d s %d) dst (%d.%d s %d) proto %d data len %d",
- ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket,
- ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
- ddpkt.Proto, len(ddpkt.Data))
-
- // Glean address info for AMT, but only if SrcNet is our net
- // (If it's not our net, then it was routed from elsewhere, and
- // we'd be filling the AMT with entries for a router.)
- if ddpkt.SrcNet >= cfg.EtherTalk.NetStart && ddpkt.SrcNet <= cfg.EtherTalk.NetEnd {
- srcAddr := ddp.Addr{Network: ddpkt.SrcNet, Node: ddpkt.SrcNode}
- aarpMachine.Learn(srcAddr, ethFrame.Src)
- // log.Printf("DDP: Gleaned that %d.%d -> %v", srcAddr.Network, srcAddr.Node, ethFrame.Src)
- }
-
- // Packet for us? First, who am I?
- myAddr, ok := aarpMachine.Address()
- if !ok {
- continue
- }
- rooter.MyDDPAddr = myAddr.Proto
-
- // Our network?
- // "The network number 0 is reserved to mean unknown; by default
- // it specifies the local network to which the node is
- // connected. Packets whose destination network number is 0 are
- // addressed to a node on the local network."
- // TODO: more generic routing
- if ddpkt.DstNet != 0 && !(ddpkt.DstNet >= cfg.EtherTalk.NetStart && ddpkt.DstNet <= cfg.EtherTalk.NetEnd) {
- // Is it for a network in the routing table?
- route := routes.LookupRoute(ddpkt.DstNet)
- if route == nil {
- log.Printf("DDP: no route for network %d", ddpkt.DstNet)
- continue
- }
-
- switch {
- case route.AURPPeer != nil:
- // Encap ethPacket.Payload into an AURP packet
- log.Printf("DDP: forwarding to AURP peer %v", route.AURPPeer.RemoteAddr)
- if _, err := route.AURPPeer.Send(route.AURPPeer.Transport.NewAppleTalkPacket(ethFrame.Payload)); err != nil {
- log.Printf("DDP: Couldn't forward packet to AURP peer: %v", err)
- }
-
- case route.EtherTalkPeer != nil:
- // Route payload to another router over EtherTalk
- // TODO: this is unlikely because we currenly only support 1 EtherTalk port
- log.Printf("DDP: forwarding to EtherTalk peer %v", route.EtherTalkPeer.PeerAddr)
- // Note: resolving AARP can block
- if err := route.EtherTalkPeer.Forward(ctx, ddpkt); err != nil {
- log.Printf("DDP: Couldn't forward packet to EtherTalk peer: %v", err)
- }
-
- default:
- log.Print("DDP: no forwarding mechanism for route; dropping packet")
- }
- continue
- }
-
- // To me?
- // "Node ID 0 indicates any router on the network"- I'm a router
- // "node ID $FF indicates either a network-wide or zone-specific
- // broadcast"- that's relevant
- if ddpkt.DstNode != 0 && ddpkt.DstNode != 0xff && ddpkt.DstNode != myAddr.Proto.Node {
- continue
- }
-
- switch ddpkt.DstSocket {
- case 1: // The RTMP socket
- rtmpCh <- ddpkt
-
- case 2: // The NIS (name information socket / NBP socket)
- if err := rooter.HandleNBP(ethFrame.Src, ddpkt); err != nil {
- log.Printf("NBP: Couldn't handle: %v", err)
- }
-
- case 4: // The AEP socket
- if err := rooter.HandleAEP(ethFrame.Src, ddpkt); err != nil {
- log.Printf("AEP: Couldn't handle: %v", err)
- }
-
- case 6: // The ZIS (zone information socket / ZIP socket)
- if err := rooter.HandleZIP(ctx, ethFrame.Src, ddpkt); err != nil {
- log.Printf("ZIP: couldn't handle: %v", err)
- }
-
- default:
- log.Printf("DDP: No handler for socket %d", ddpkt.DstSocket)
- }
-
- default:
- log.Printf("Read unknown packet %s -> %s with payload %x", ethFrame.Src, ethFrame.Dst, ethFrame.Payload)
-
- }
- }
+ etherTalkPort.Serve(ctx)
}()
// ----------------------------- AURP inbound -----------------------------
@@ -593,79 +472,37 @@ func main() {
log.Printf("AURP: Couldn't unmarshal encapsulated DDP packet: %v", err)
continue
}
- log.Printf("DDP/AURP: Got %d.%d.%d -> %d.%d.%d proto %d data len %d",
- ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket,
- ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
- ddpkt.Proto, len(ddpkt.Data))
+ // log.Printf("DDP/AURP: Got %d.%d.%d -> %d.%d.%d proto %d data len %d",
+ // ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket,
+ // ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
+ // ddpkt.Proto, len(ddpkt.Data))
- // Route the packet
-
- // Check and adjust the Hop Count
- // Note the ddp package doesn't make this simple
- hopCount := (ddpkt.Size & 0x3C00) >> 10
- if hopCount >= 15 {
- log.Printf("DDP/AURP: hop count exceeded (%d >= 15)", hopCount)
- continue
- }
- hopCount++
- ddpkt.Size &^= 0x3C00
- ddpkt.Size |= hopCount << 10
-
- if ddpkt.DstNet < cfg.EtherTalk.NetStart || ddpkt.DstNet > cfg.EtherTalk.NetEnd {
- // Is it a network in the routing table?
- route := routes.LookupRoute(ddpkt.DstNet)
- if route == nil {
- log.Printf("DDP/AURP: no route for packet (dstnet %d); dropping packet", ddpkt.DstNet)
+ // Is it addressed to me?
+ var localPort *router.EtherTalkPort
+ for _, port := range rooter.Ports {
+ if ddpkt.DstNet >= port.NetStart && ddpkt.DstNet <= port.NetEnd {
+ localPort = port
break
}
-
- switch {
- case route.AURPPeer != nil:
- // Routing between AURP peers... bit weird but OK
- log.Printf("DDP/AURP: forwarding to AURP peer %v", route.AURPPeer.RemoteAddr)
- outPkt, err := ddp.ExtMarshal(*ddpkt)
- if err != nil {
- log.Printf("DDP/AURP: Couldn't re-marshal packet: %v", err)
- break
- }
- if _, err := route.AURPPeer.Send(route.AURPPeer.Transport.NewAppleTalkPacket(outPkt)); err != nil {
- log.Printf("DDP/AURP: Couldn't forward packet to AURP peer: %v", err)
- }
-
- case route.EtherTalkPeer != nil:
- // AURP peer -> EtherTalk peer
- // Note: resolving AARP can block
- log.Printf("DDP/AURP: forwarding to EtherTalk peer %v", route.EtherTalkPeer.PeerAddr)
- if err := route.EtherTalkPeer.Forward(ctx, ddpkt); err != nil {
- log.Printf("DDP/AURP: Couldn't forward packet to EtherTalk peer: %v", err)
- }
-
- default:
- log.Print("DDP/AURP: no forwarding mechanism for route; dropping packet")
-
- }
- continue
}
-
- // Is it addressed to me? Is it NBP?
- if ddpkt.DstNode == 0 { // Node 0 = any router for the network = me
+ if ddpkt.DstNode == 0 && localPort != nil { // Node 0 = any router for the network = me
+ // Is it NBP? FwdReq needs translating.
if ddpkt.DstSocket != 2 {
// Something else?? TODO
log.Printf("DDP/AURP: I don't have anything 'listening' on socket %d", ddpkt.DstSocket)
continue
}
- // It's NBP
- if err := rooter.HandleNBPInAURP(pr, ddpkt); err != nil {
+ // It's NBP, specifically it should be a FwdReq
+ if err := rooter.HandleNBPFromAURP(ctx, ddpkt); err != nil {
log.Printf("NBP/DDP/AURP: %v", err)
}
continue
}
- // Note: resolving AARP can block
- if err := rooter.SendEtherTalkDDP(ctx, ddpkt); err != nil {
- log.Printf("DDP/AURP: couldn't send Ethertalk out: %v", err)
+ // Route the packet!
+ if err := rooter.Forward(ctx, ddpkt); err != nil {
+ log.Printf("DDP/AURP: Couldn't route packet: %v", err)
}
- continue
default:
log.Printf("AURP: Got unknown packet type %v", dh.PacketType)
diff --git a/router/aarp.go b/router/aarp.go
index 7397b7a..2fdeb11 100644
--- a/router/aarp.go
+++ b/router/aarp.go
@@ -73,6 +73,8 @@ type AARPMachine struct {
cfg *Config
pcapHandle *pcap.Handle
+ incomingCh chan *ethertalk.Packet
+
// The Run goroutine is responsible for all writes to myAddr.Proto and
// probes, so this mutex is not used to enforce a single writer, only
// consistent reads
@@ -90,6 +92,7 @@ func NewAARPMachine(cfg *Config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr
addressMappingTable: new(addressMappingTable),
cfg: cfg,
pcapHandle: pcapHandle,
+ incomingCh: make(chan *ethertalk.Packet, 1024), // arbitrary capacity
myAddr: aarp.AddrPair{
Hardware: myHWAddr,
},
@@ -97,6 +100,14 @@ func NewAARPMachine(cfg *Config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr
}
}
+// Handle handles a packet.
+func (a *AARPMachine) Handle(ctx context.Context, pkt *ethertalk.Packet) {
+ select {
+ case <-ctx.Done():
+ case a.incomingCh <- pkt:
+ }
+}
+
// Address returns the address of this node, and reports if the address is valid
// (i.e. not tentative).
func (a *AARPMachine) Address() (aarp.AddrPair, bool) {
@@ -123,7 +134,7 @@ func (a *AARPMachine) status(ctx context.Context) (any, error) {
}
// Run executes the machine.
-func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Packet) error {
+func (a *AARPMachine) Run(ctx context.Context) error {
ctx, done := status.AddItem(ctx, "AARP", aarpStatusTemplate, a.status)
defer done()
@@ -165,9 +176,9 @@ func (a *AARPMachine) Run(ctx context.Context, incomingCh <-chan *ethertalk.Pack
log.Printf("Couldn't broadcast a Probe: %v", err)
}
- case ethFrame, ok := <-incomingCh:
+ case ethFrame, ok := <-a.incomingCh:
if !ok {
- incomingCh = nil
+ a.incomingCh = nil
}
var aapkt aarp.Packet
diff --git a/router/aep.go b/router/aep.go
index 40c692a..8a3d99b 100644
--- a/router/aep.go
+++ b/router/aep.go
@@ -17,14 +17,14 @@
package router
import (
+ "context"
"fmt"
"gitea.drjosh.dev/josh/jrouter/atalk/aep"
"github.com/sfiera/multitalk/pkg/ddp"
- "github.com/sfiera/multitalk/pkg/ethernet"
)
-func (rtr *Router) HandleAEP(src ethernet.Addr, ddpkt *ddp.ExtPacket) error {
+func (rtr *Router) HandleAEP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
if ddpkt.Proto != ddp.ProtoAEP {
return fmt.Errorf("invalid DDP type %d on socket 4", ddpkt.Proto)
}
@@ -47,7 +47,7 @@ func (rtr *Router) HandleAEP(src ethernet.Addr, ddpkt *ddp.ExtPacket) error {
ddpkt.DstSocket, ddpkt.SrcSocket = ddpkt.SrcSocket, ddpkt.DstSocket
ddpkt.Data[0] = byte(aep.EchoReply)
- return rtr.sendEtherTalkDDP(src, ddpkt)
+ return rtr.Output(ctx, ddpkt)
default:
return fmt.Errorf("invalid AEP function %d", ep.Function)
diff --git a/router/nbp.go b/router/nbp.go
index ec7a7e5..673b9d3 100644
--- a/router/nbp.go
+++ b/router/nbp.go
@@ -17,16 +17,17 @@
package router
import (
+ "context"
"fmt"
"log"
+ "slices"
"gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/nbp"
"github.com/sfiera/multitalk/pkg/ddp"
- "github.com/sfiera/multitalk/pkg/ethernet"
)
-func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) error {
+func (port *EtherTalkPort) HandleNBP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
if ddpkt.Proto != ddp.ProtoNBP {
return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto)
}
@@ -41,130 +42,180 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
switch nbpkt.Function {
case nbp.FunctionLkUp:
// when in AppleTalk, do as Apple Internet Router does...
- outDDP, err := rtr.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, &nbpkt.Tuples[0])
+ outDDP, err := port.helloWorldThisIsMe(nbpkt.NBPID, &nbpkt.Tuples[0])
if err != nil || outDDP == nil {
return err
}
log.Print("NBP: Replying to LkUp with LkUp-Reply for myself")
- return rtr.sendEtherTalkDDP(srcHWAddr, outDDP)
+ // Note: AARP can block
+ return port.Send(ctx, outDDP)
+
+ case nbp.FunctionFwdReq:
+ return port.Router.handleNBPFwdReq(ctx, ddpkt, nbpkt)
case nbp.FunctionBrRq:
- // There must be 1!
- tuple := &nbpkt.Tuples[0]
+ return port.handleNBPBrRq(ctx, ddpkt, nbpkt)
- zones := rtr.ZoneTable.LookupName(tuple.Zone)
+ default:
+ return fmt.Errorf("TODO: handle function %v", nbpkt.Function)
+ }
+}
- for _, z := range zones {
- if z.Local {
- // 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."
- nbpkt.Function = nbp.FunctionLkUp
- nbpRaw, err := nbpkt.Marshal()
- if err != nil {
- return fmt.Errorf("couldn't marshal LkUp: %v", err)
- }
+func (port *EtherTalkPort) handleNBPBrRq(ctx context.Context, ddpkt *ddp.ExtPacket, nbpkt *nbp.Packet) error {
+ // A BrRq was addressed to us. The sender (on a local network) is aware that
+ // the network is extended and routed, and instead of broadcasting a LkUp
+ // itself, is asking us to do it.
- outDDP := ddp.ExtPacket{
- ExtHeader: ddp.ExtHeader{
- Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)),
- Cksum: 0,
- SrcNet: ddpkt.SrcNet,
- SrcNode: ddpkt.SrcNode,
- SrcSocket: ddpkt.SrcSocket,
- DstNet: 0x0000, // Local network broadcast
- DstNode: 0xFF, // Broadcast node address within the dest network
- DstSocket: 2,
- Proto: ddp.ProtoNBP,
- },
- Data: nbpRaw,
- }
+ // There must be 1!
+ tuple := &nbpkt.Tuples[0]
- log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple)
- if err := rtr.ZoneMulticastEtherTalkDDP(tuple.Zone, &outDDP); err != nil {
- return err
- }
+ // This logic would be required on a non-extended network:
+ // if tuple.Zone == "" || tuple.Zone == "*" {
+ // tuple.Zone = port.DefaultZoneName
+ // }
- // But also...if we match the query, reply as though it was a LkUp
- outDDP2, err := rtr.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, tuple)
- if err != nil {
- return err
- }
- if outDDP2 == nil {
- continue
- }
- log.Print("NBP: Replying to BrRq with LkUp-Reply for myself")
- if err := rtr.sendEtherTalkDDP(srcHWAddr, outDDP2); err != nil {
- return err
- }
+ zones := port.Router.ZoneTable.LookupName(tuple.Zone)
- continue
- }
-
- route := rtr.RouteTable.LookupRoute(z.Network)
- if route == nil {
- return fmt.Errorf("no route for network %d", z.Network)
- }
- peer := route.AURPPeer
- if peer == nil {
- return fmt.Errorf("nil peer for route for network %d", z.Network)
- }
-
- // Translate it into a FwdReq and route it to the
- // routers with the appropriate zone(s).
- nbpkt.Function = nbp.FunctionFwdReq
+ for _, z := range zones {
+ if outPort := z.LocalPort; outPort != nil {
+ // If it's for a local zone, translate it to a LkUp and broadcast
+ // out the corresponding 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."
+ nbpkt.Function = nbp.FunctionLkUp
nbpRaw, err := nbpkt.Marshal()
if err != nil {
- return fmt.Errorf("couldn't marshal FwdReq: %v", err)
+ return fmt.Errorf("couldn't marshal LkUp: %v", err)
}
outDDP := ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)),
Cksum: 0,
- SrcNet: ddpkt.SrcNet,
- SrcNode: ddpkt.SrcNode,
- SrcSocket: ddpkt.SrcSocket,
- DstNet: z.Network,
- DstNode: 0x00, // Any router for the dest network
+ SrcNet: port.MyAddr.Network,
+ SrcNode: port.MyAddr.Node,
+ SrcSocket: 2,
+ DstNet: 0x0000, // Local network broadcast
+ DstNode: 0xFF, // Broadcast node address within the dest network
DstSocket: 2,
Proto: ddp.ProtoNBP,
},
Data: nbpRaw,
}
- outDDPRaw, err := ddp.ExtMarshal(outDDP)
- if err != nil {
+ log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple)
+ if err := outPort.ZoneMulticast(tuple.Zone, &outDDP); err != nil {
return err
}
- log.Printf("NBP: Sending FwdReq to %v for tuple %v", peer.RemoteAddr, tuple)
-
- if _, err := peer.Send(peer.Transport.NewAppleTalkPacket(outDDPRaw)); err != nil {
- return fmt.Errorf("sending FwdReq on to peer: %w", err)
+ // But also...if we match the query, reply as though it was a LkUp
+ // This uses the *input* port information.
+ outDDP2, err := port.helloWorldThisIsMe(nbpkt.NBPID, tuple)
+ if err != nil {
+ return err
}
+ if outDDP2 == nil {
+ continue
+ }
+ log.Print("NBP: Replying to BrRq directly with LkUp-Reply for myself")
+ // Can reply to this BrRq on the same port we got it, because it
+ // wasn't routed
+ if err := port.Send(ctx, outDDP2); err != nil {
+ return err
+ }
+
+ continue
}
- default:
- return fmt.Errorf("TODO: handle function %v", nbpkt.Function)
- }
+ // The zone table row is *not* for a local network.
+ // Translate it into a FwdReq and route that to the routers that do have
+ // that zone as a local network.
+ nbpkt.Function = nbp.FunctionFwdReq
+ nbpRaw, err := nbpkt.Marshal()
+ if err != nil {
+ return fmt.Errorf("couldn't marshal FwdReq: %v", err)
+ }
+ outDDP := &ddp.ExtPacket{
+ ExtHeader: ddp.ExtHeader{
+ Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)),
+ Cksum: 0,
+ SrcNet: ddpkt.SrcNet,
+ SrcNode: ddpkt.SrcNode,
+ SrcSocket: ddpkt.SrcSocket,
+ DstNet: z.Network,
+ DstNode: 0x00, // Any router for the dest network
+ DstSocket: 2,
+ Proto: ddp.ProtoNBP,
+ },
+ Data: nbpRaw,
+ }
+
+ if err := port.Router.Output(ctx, outDDP); err != nil {
+ return err
+ }
+ }
return nil
}
-func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *nbp.Tuple) (*ddp.ExtPacket, error) {
+func (rtr *Router) handleNBPFwdReq(ctx context.Context, ddpkt *ddp.ExtPacket, nbpkt *nbp.Packet) error {
+ // A FwdReq was addressed to us. That means a remote router thinks the
+ // zone is available on one or more of our local networks.
+
+ // There must be 1!
+ tuple := &nbpkt.Tuples[0]
+
+ for _, outPort := range rtr.Ports {
+ if !slices.Contains(outPort.AvailableZones, tuple.Zone) {
+ continue
+ }
+ log.Printf("NBP: Converting FwdReq to LkUp (%v)", tuple)
+
+ // Convert it to a LkUp and broadcast on the corresponding port
+ nbpkt.Function = nbp.FunctionLkUp
+ nbpRaw, err := nbpkt.Marshal()
+ if err != nil {
+ return fmt.Errorf("couldn't marshal LkUp: %v", err)
+ }
+
+ // Inside AppleTalk SE, pp 8-20:
+ // "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
+
+ if err := outPort.ZoneMulticast(tuple.Zone, ddpkt); err != nil {
+ return err
+ }
+
+ // But also... if it matches us, reply directly with a LkUp-Reply of our own
+ outDDP, err := outPort.helloWorldThisIsMe(nbpkt.NBPID, tuple)
+ if err != nil || outDDP == nil {
+ return err
+ }
+ if err := rtr.Output(ctx, outDDP); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Returns an NBP LkUp-Reply for the router itself, with the address from this port.
+func (port *EtherTalkPort) helloWorldThisIsMe(nbpID uint8, tuple *nbp.Tuple) (*ddp.ExtPacket, error) {
if tuple.Object != "jrouter" && tuple.Object != "=" {
return nil, nil
}
if tuple.Type != "AppleRouter" && tuple.Type != "=" {
return nil, nil
}
- if tuple.Zone != rtr.Config.EtherTalk.ZoneName && tuple.Zone != "*" && tuple.Zone != "" {
+ if tuple.Zone != port.DefaultZoneName && tuple.Zone != "*" && tuple.Zone != "" {
return nil, nil
}
respPkt := &nbp.Packet{
@@ -172,13 +223,13 @@ func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *
NBPID: nbpID,
Tuples: []nbp.Tuple{
{
- Network: rtr.MyDDPAddr.Network,
- Node: rtr.MyDDPAddr.Node,
+ Network: port.MyAddr.Network,
+ Node: port.MyAddr.Node,
Socket: 253,
Enumerator: 0,
Object: "jrouter",
Type: "AppleRouter",
- Zone: rtr.Config.EtherTalk.ZoneName,
+ Zone: port.DefaultZoneName,
},
},
}
@@ -186,15 +237,25 @@ func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *
if err != nil {
return nil, fmt.Errorf("couldn't marshal LkUp-Reply: %v", err)
}
+
+ // Inside AppleTalk SE, pp 7-16:
+ // "In BrRq, FwdReq, and LkUp packets, which carry only a single tuple, the
+ // address field contains the internet address of the requester, allowing
+ // the responder to address the LkUp-Reply datagram."
+ // Inside AppleTalk SE, pp 8-20:
+ // "Note: NBP is defined so that the router's NBP process does not
+ // participate in the NBP response process; the response is sent directly to
+ // the original requester through DDP. It is important that the original
+ // requester's field be obtained from the address field of the NBP tuple."
return &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{
Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0,
- DstNet: ddpkt.SrcNet,
- DstNode: ddpkt.SrcNode,
- DstSocket: ddpkt.SrcSocket,
- SrcNet: rtr.MyDDPAddr.Network,
- SrcNode: rtr.MyDDPAddr.Node,
+ DstNet: tuple.Network,
+ DstNode: tuple.Node,
+ DstSocket: tuple.Socket,
+ SrcNet: port.MyAddr.Network,
+ SrcNode: port.MyAddr.Node,
SrcSocket: 2,
Proto: ddp.ProtoNBP,
},
diff --git a/router/nbp_aurp.go b/router/nbp_aurp.go
index 306751e..31b46af 100644
--- a/router/nbp_aurp.go
+++ b/router/nbp_aurp.go
@@ -17,14 +17,14 @@
package router
import (
+ "context"
"fmt"
- "log"
"gitea.drjosh.dev/josh/jrouter/atalk/nbp"
"github.com/sfiera/multitalk/pkg/ddp"
)
-func (rtr *Router) HandleNBPInAURP(peer *AURPPeer, ddpkt *ddp.ExtPacket) error {
+func (rtr *Router) HandleNBPFromAURP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
if ddpkt.Proto != ddp.ProtoNBP {
return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto)
}
@@ -36,49 +36,5 @@ func (rtr *Router) HandleNBPInAURP(peer *AURPPeer, ddpkt *ddp.ExtPacket) error {
// 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")
- }
- tuple := &nbpkt.Tuples[0]
-
- if tuple.Zone != rtr.Config.EtherTalk.ZoneName {
- return fmt.Errorf("FwdReq querying zone %q, which is not our zone", tuple.Zone)
- }
-
- // TODO: Route the FwdReq to another router if it's not our zone
-
- log.Printf("NBP/DDP/AURP: Converting FwdReq to LkUp (%v)", tuple)
-
- // Convert it to a LkUp and broadcast on EtherTalk
- 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
-
- if err := rtr.ZoneMulticastEtherTalkDDP(tuple.Zone, ddpkt); err != nil {
- return err
- }
-
- // But also... if it matches us, reply directly with a LkUp-Reply of our own
- outDDP, err := rtr.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, tuple)
- if err != nil || outDDP == nil {
- return err
- }
- log.Print("NBP/DDP/AURP: Replying to BrRq with LkUp-Reply for myself")
- outDDPRaw, err := ddp.ExtMarshal(*outDDP)
- if err != nil {
- return err
- }
- _, err = peer.Send(peer.Transport.NewAppleTalkPacket(outDDPRaw))
- return err
+ return rtr.handleNBPFwdReq(ctx, ddpkt, nbpkt)
}
diff --git a/router/peer_aurp.go b/router/peer_aurp.go
index d36eb56..85432cc 100644
--- a/router/peer_aurp.go
+++ b/router/peer_aurp.go
@@ -115,7 +115,7 @@ type AURPPeer struct {
ReceiveCh chan aurp.Packet
// Routing table (the peer will add/remove/update routes)
- RoutingTable *RoutingTable
+ RoutingTable *RouteTable
// Zone table (the peer will add/remove/update zones)
ZoneTable *ZoneTable
@@ -125,6 +125,15 @@ type AURPPeer struct {
sstate SenderState
}
+func (p *AURPPeer) Forward(ddpkt *ddp.ExtPacket) error {
+ outPkt, err := ddp.ExtMarshal(*ddpkt)
+ if err != nil {
+ return err
+ }
+ _, err = p.Send(p.Transport.NewAppleTalkPacket(outPkt))
+ return err
+}
+
func (p *AURPPeer) ReceiverState() ReceiverState {
p.mu.RLock()
defer p.mu.RUnlock()
@@ -596,7 +605,7 @@ func (p *AURPPeer) Handle(ctx context.Context) error {
case *aurp.ZIRspPacket:
log.Printf("AURP Peer: Learned about these zones: %v", pkt.Zones)
for _, zt := range pkt.Zones {
- p.ZoneTable.Upsert(ddp.Network(zt.Network), zt.Name, false)
+ p.ZoneTable.Upsert(ddp.Network(zt.Network), zt.Name, nil)
}
case *aurp.GDZLReqPacket:
diff --git a/router/peer_eth.go b/router/peer_eth.go
index 046687e..4b181a2 100644
--- a/router/peer_eth.go
+++ b/router/peer_eth.go
@@ -19,29 +19,25 @@ package router
import (
"context"
- "github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp"
- "github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
)
-// EtherTalkPeer holds data needed to exchange routes and zones with another
-// router on the EtherTalk network.
+// EtherTalkPeer holds data needed to forward packets to another router on the
+// EtherTalk network.
type EtherTalkPeer struct {
- PcapHandle *pcap.Handle
- MyHWAddr ethernet.Addr
- AARP *AARPMachine
- PeerAddr ddp.Addr
+ Port *EtherTalkPort
+ PeerAddr ddp.Addr
}
// Forward forwards a DDP packet to the next router.
func (p *EtherTalkPeer) Forward(ctx context.Context, pkt *ddp.ExtPacket) error {
// TODO: AARP resolution can block
- de, err := p.AARP.Resolve(ctx, p.PeerAddr)
+ de, err := p.Port.AARPMachine.Resolve(ctx, p.PeerAddr)
if err != nil {
return err
}
- outFrame, err := ethertalk.AppleTalk(p.MyHWAddr, *pkt)
+ outFrame, err := ethertalk.AppleTalk(p.Port.EthernetAddr, *pkt)
if err != nil {
return err
}
@@ -50,5 +46,5 @@ func (p *EtherTalkPeer) Forward(ctx context.Context, pkt *ddp.ExtPacket) error {
if err != nil {
return err
}
- return p.PcapHandle.WritePacketData(outFrameRaw)
+ return p.Port.PcapHandle.WritePacketData(outFrameRaw)
}
diff --git a/router/port.go b/router/port.go
new file mode 100644
index 0000000..eb8aad6
--- /dev/null
+++ b/router/port.go
@@ -0,0 +1,193 @@
+/*
+ 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"
+ "errors"
+ "io"
+ "log"
+
+ "gitea.drjosh.dev/josh/jrouter/atalk"
+ "github.com/google/gopacket/pcap"
+ "github.com/sfiera/multitalk/pkg/ddp"
+ "github.com/sfiera/multitalk/pkg/ethernet"
+ "github.com/sfiera/multitalk/pkg/ethertalk"
+)
+
+// EtherTalkPort is all the data and helpers needed for EtherTalk on one port.
+type EtherTalkPort struct {
+ Device string
+ EthernetAddr ethernet.Addr
+ NetStart ddp.Network
+ NetEnd ddp.Network
+ MyAddr ddp.Addr
+ DefaultZoneName string
+ AvailableZones []string
+ PcapHandle *pcap.Handle
+ AARPMachine *AARPMachine
+ Router *Router
+}
+
+func (port *EtherTalkPort) Serve(ctx context.Context) {
+ for {
+ if ctx.Err() != nil {
+ return
+ }
+
+ rawPkt, _, err := port.PcapHandle.ReadPacketData()
+ if errors.Is(err, pcap.NextErrorTimeoutExpired) {
+ continue
+ }
+ if errors.Is(err, io.EOF) || errors.Is(err, pcap.NextErrorNoMorePackets) {
+ return
+ }
+ if err != nil {
+ log.Printf("Couldn't read AppleTalk / AARP packet data: %v", err)
+ return
+ }
+
+ ethFrame := new(ethertalk.Packet)
+ if err := ethertalk.Unmarshal(rawPkt, ethFrame); err != nil {
+ log.Printf("Couldn't unmarshal EtherTalk frame: %v", err)
+ continue
+ }
+
+ // Ignore if sent by me
+ if ethFrame.Src == port.EthernetAddr {
+ continue
+ }
+
+ switch ethFrame.SNAPProto {
+ case ethertalk.AARPProto:
+ // log.Print("Got an AARP frame")
+ port.AARPMachine.Handle(ctx, ethFrame)
+
+ case ethertalk.AppleTalkProto:
+ // log.Print("Got an AppleTalk frame")
+ ddpkt := new(ddp.ExtPacket)
+ if err := ddp.ExtUnmarshal(ethFrame.Payload, ddpkt); err != nil {
+ log.Printf("Couldn't unmarshal DDP packet: %v", err)
+ continue
+ }
+ // log.Printf("DDP: src (%d.%d s %d) dst (%d.%d s %d) proto %d data len %d",
+ // ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket,
+ // ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
+ // ddpkt.Proto, len(ddpkt.Data))
+
+ // Glean address info for AMT, but only if SrcNet is our net
+ // (If it's not our net, then it was routed from elsewhere, and
+ // we'd be filling the AMT with entries for a router.)
+ if ddpkt.SrcNet >= port.NetStart && ddpkt.SrcNet <= port.NetEnd {
+ srcAddr := ddp.Addr{Network: ddpkt.SrcNet, Node: ddpkt.SrcNode}
+ port.AARPMachine.Learn(srcAddr, ethFrame.Src)
+ // log.Printf("DDP: Gleaned that %d.%d -> %v", srcAddr.Network, srcAddr.Node, ethFrame.Src)
+ }
+
+ // Packet for us? First, who am I?
+ myAddr, ok := port.AARPMachine.Address()
+ if !ok {
+ continue
+ }
+ port.MyAddr = myAddr.Proto
+
+ // Our network?
+ // "The network number 0 is reserved to mean unknown; by default
+ // it specifies the local network to which the node is
+ // connected. Packets whose destination network number is 0 are
+ // addressed to a node on the local network."
+ // TODO: more generic routing
+ if ddpkt.DstNet != 0 && !(ddpkt.DstNet >= port.NetStart && ddpkt.DstNet <= port.NetEnd) {
+ // Is it for a network in the routing table?
+ if err := port.Router.Forward(ctx, ddpkt); err != nil {
+ log.Printf("DDP: Couldn't forward packet: %v", err)
+ }
+ continue
+ }
+
+ // To me?
+ // "Node ID 0 indicates any router on the network"- I'm a router
+ // "node ID $FF indicates either a network-wide or zone-specific
+ // broadcast"- that's relevant
+ if ddpkt.DstNode != 0 && ddpkt.DstNode != 0xff && ddpkt.DstNode != myAddr.Proto.Node {
+ continue
+ }
+
+ switch ddpkt.DstSocket {
+ case 1: // The RTMP socket
+ if err := port.HandleRTMP(ctx, ddpkt); err != nil {
+ log.Printf("RTMP: Couldn't handle: %v", err)
+ }
+
+ case 2: // The NIS (name information socket / NBP socket)
+ if err := port.HandleNBP(ctx, ddpkt); err != nil {
+ log.Printf("NBP: Couldn't handle: %v", err)
+ }
+
+ case 4: // The AEP socket
+ if err := port.Router.HandleAEP(ctx, ddpkt); err != nil {
+ log.Printf("AEP: Couldn't handle: %v", err)
+ }
+
+ case 6: // The ZIS (zone information socket / ZIP socket)
+ if err := port.HandleZIP(ctx, ddpkt); err != nil {
+ log.Printf("ZIP: couldn't handle: %v", err)
+ }
+
+ default:
+ log.Printf("DDP: No handler for socket %d", ddpkt.DstSocket)
+ }
+
+ default:
+ log.Printf("Read unknown packet %s -> %s with payload %x", ethFrame.Src, ethFrame.Dst, ethFrame.Payload)
+
+ }
+ }
+}
+
+func (port *EtherTalkPort) Send(ctx context.Context, pkt *ddp.ExtPacket) error {
+ dstEth := ethertalk.AppleTalkBroadcast
+ if pkt.DstNode != 0xFF {
+ de, err := port.AARPMachine.Resolve(ctx, ddp.Addr{Network: pkt.DstNet, Node: pkt.DstNode})
+ if err != nil {
+ return err
+ }
+ dstEth = de
+ }
+ return port.send(dstEth, pkt)
+}
+
+func (port *EtherTalkPort) Broadcast(pkt *ddp.ExtPacket) error {
+ return port.send(ethertalk.AppleTalkBroadcast, pkt)
+}
+
+func (port *EtherTalkPort) ZoneMulticast(zone string, pkt *ddp.ExtPacket) error {
+ return port.send(atalk.MulticastAddr(zone), pkt)
+}
+
+func (port *EtherTalkPort) send(dstEth ethernet.Addr, pkt *ddp.ExtPacket) error {
+ outFrame, err := ethertalk.AppleTalk(port.EthernetAddr, *pkt)
+ if err != nil {
+ return err
+ }
+ outFrame.Dst = dstEth
+ outFrameRaw, err := ethertalk.Marshal(*outFrame)
+ if err != nil {
+ return err
+ }
+ return port.PcapHandle.WritePacketData(outFrameRaw)
+}
diff --git a/router/route.go b/router/route.go
index f629821..a399836 100644
--- a/router/route.go
+++ b/router/route.go
@@ -35,8 +35,9 @@ type Route struct {
LastSeen time.Time
// Exactly one of the following should be set
- AURPPeer *AURPPeer
- EtherTalkPeer *EtherTalkPeer
+ AURPPeer *AURPPeer // Next hop is this peer router (over AURP)
+ EtherTalkPeer *EtherTalkPeer // Next hop is this peer router (over EtherTalk)
+ EtherTalkDirect *EtherTalkPort // Directly connected to this network (via EtherTalk)
}
func (r Route) LastSeenAgo() string {
@@ -46,18 +47,33 @@ func (r Route) LastSeenAgo() string {
return fmt.Sprintf("%v ago", time.Since(r.LastSeen).Truncate(time.Millisecond))
}
-type RoutingTable struct {
+type RouteTable struct {
mu sync.Mutex
routes map[*Route]struct{}
}
-func NewRoutingTable() *RoutingTable {
- return &RoutingTable{
+func NewRouteTable() *RouteTable {
+ return &RouteTable{
routes: make(map[*Route]struct{}),
}
}
-func (rt *RoutingTable) Dump() []Route {
+func (rt *RouteTable) InsertEtherTalkDirect(port *EtherTalkPort) {
+ r := &Route{
+ Extended: true,
+ NetStart: port.NetStart,
+ NetEnd: port.NetEnd,
+ Distance: 0, // we're connected directly
+ LastSeen: time.Now(),
+ EtherTalkDirect: port,
+ }
+
+ rt.mu.Lock()
+ defer rt.mu.Unlock()
+ rt.routes[r] = struct{}{}
+}
+
+func (rt *RouteTable) Dump() []Route {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -68,7 +84,7 @@ func (rt *RoutingTable) Dump() []Route {
return table
}
-func (rt *RoutingTable) LookupRoute(network ddp.Network) *Route {
+func (rt *RouteTable) LookupRoute(network ddp.Network) *Route {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -92,7 +108,7 @@ func (rt *RoutingTable) LookupRoute(network ddp.Network) *Route {
return bestRoute
}
-func (rt *RoutingTable) DeleteAURPPeer(peer *AURPPeer) {
+func (rt *RouteTable) DeleteAURPPeer(peer *AURPPeer) {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -103,7 +119,7 @@ func (rt *RoutingTable) DeleteAURPPeer(peer *AURPPeer) {
}
}
-func (rt *RoutingTable) DeleteAURPPeerNetwork(peer *AURPPeer, network ddp.Network) {
+func (rt *RouteTable) DeleteAURPPeerNetwork(peer *AURPPeer, network ddp.Network) {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -114,7 +130,7 @@ func (rt *RoutingTable) DeleteAURPPeerNetwork(peer *AURPPeer, network ddp.Networ
}
}
-func (rt *RoutingTable) UpdateAURPRouteDistance(peer *AURPPeer, network ddp.Network, distance uint8) {
+func (rt *RouteTable) UpdateAURPRouteDistance(peer *AURPPeer, network ddp.Network, distance uint8) {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -126,7 +142,7 @@ func (rt *RoutingTable) UpdateAURPRouteDistance(peer *AURPPeer, network ddp.Netw
}
}
-func (rt *RoutingTable) UpsertEthRoute(peer *EtherTalkPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
+func (rt *RouteTable) UpsertEtherTalkRoute(peer *EtherTalkPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
if netStart > netEnd {
return fmt.Errorf("invalid network range [%d, %d]", netStart, netEnd)
}
@@ -169,7 +185,7 @@ func (rt *RoutingTable) UpsertEthRoute(peer *EtherTalkPeer, extended bool, netSt
return nil
}
-func (rt *RoutingTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
+func (rt *RouteTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
if netStart > netEnd {
return fmt.Errorf("invalid network range [%d, %d]", netStart, netEnd)
}
@@ -192,7 +208,7 @@ func (rt *RoutingTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart,
return nil
}
-func (rt *RoutingTable) ValidRoutes() []*Route {
+func (rt *RouteTable) ValidRoutes() []*Route {
rt.mu.Lock()
defer rt.mu.Unlock()
valid := make([]*Route, 0, len(rt.routes))
diff --git a/router/router.go b/router/router.go
index 916e3a7..7295076 100644
--- a/router/router.go
+++ b/router/router.go
@@ -18,53 +18,56 @@ package router
import (
"context"
+ "fmt"
- "gitea.drjosh.dev/josh/jrouter/atalk"
- "github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp"
- "github.com/sfiera/multitalk/pkg/ethernet"
- "github.com/sfiera/multitalk/pkg/ethertalk"
)
type Router struct {
- Config *Config
- PcapHandle *pcap.Handle
- MyHWAddr ethernet.Addr
- MyDDPAddr ddp.Addr
- AARPMachine *AARPMachine
- RouteTable *RoutingTable
- ZoneTable *ZoneTable
+ Config *Config
+ RouteTable *RouteTable
+ ZoneTable *ZoneTable
+ Ports []*EtherTalkPort
}
-func (rtr *Router) SendEtherTalkDDP(ctx context.Context, pkt *ddp.ExtPacket) error {
- dstEth := ethertalk.AppleTalkBroadcast
- if pkt.DstNode != 0xFF {
- de, err := rtr.AARPMachine.Resolve(ctx, ddp.Addr{Network: pkt.DstNet, Node: pkt.DstNode})
- if err != nil {
- return err
- }
- dstEth = de
+// Forward increments the hop count, then outputs the packet in the direction
+// of the destination.
+func (rtr *Router) Forward(ctx context.Context, ddpkt *ddp.ExtPacket) error {
+ // Check and adjust the Hop Count
+ // Note the ddp package doesn't make this simple
+ hopCount := (ddpkt.Size & 0x3C00) >> 10
+ if hopCount >= 15 {
+ return fmt.Errorf("hop count exceeded (%d >= 15)", hopCount)
}
- return rtr.sendEtherTalkDDP(dstEth, pkt)
+ hopCount++
+ ddpkt.Size &^= 0x3C00
+ ddpkt.Size |= hopCount << 10
+
+ return rtr.Output(ctx, ddpkt)
}
-func (rtr *Router) BroadcastEtherTalkDDP(pkt *ddp.ExtPacket) error {
- return rtr.sendEtherTalkDDP(ethertalk.AppleTalkBroadcast, pkt)
-}
+// Output outputs the packet in the direction of the destination.
+// (It does not check or adjust the hop count.)
+func (rtr *Router) Output(ctx context.Context, ddpkt *ddp.ExtPacket) error {
+ switch route := rtr.RouteTable.LookupRoute(ddpkt.DstNet); {
+ case route == nil:
+ return fmt.Errorf("no route for packet (dstnet %d); dropping packet", ddpkt.DstNet)
-func (rtr *Router) ZoneMulticastEtherTalkDDP(zone string, pkt *ddp.ExtPacket) error {
- return rtr.sendEtherTalkDDP(atalk.MulticastAddr(zone), pkt)
-}
+ case route.AURPPeer != nil:
+ // log.Printf("Forwarding packet to AURP peer %v", route.AURPPeer.RemoteAddr)
+ return route.AURPPeer.Forward(ddpkt)
-func (rtr *Router) sendEtherTalkDDP(dstEth ethernet.Addr, pkt *ddp.ExtPacket) error {
- outFrame, err := ethertalk.AppleTalk(rtr.MyHWAddr, *pkt)
- if err != nil {
- return err
+ case route.EtherTalkPeer != nil:
+ // log.Printf("Forwarding to EtherTalk peer %v", route.EtherTalkPeer.PeerAddr)
+ // Note: resolving AARP can block
+ return route.EtherTalkPeer.Forward(ctx, ddpkt)
+
+ case route.EtherTalkDirect != nil:
+ // log.Printf("Outputting to EtherTalk directly")
+ // Note: resolving AARP can block
+ return route.EtherTalkDirect.Send(ctx, ddpkt)
+
+ default:
+ return fmt.Errorf("no forwarding mechanism for route! %+v", route)
}
- outFrame.Dst = dstEth
- outFrameRaw, err := ethertalk.Marshal(*outFrame)
- if err != nil {
- return err
- }
- return rtr.PcapHandle.WritePacketData(outFrameRaw)
}
diff --git a/router/rtmp.go b/router/rtmp.go
index c7aea62..bdfb6b5 100644
--- a/router/rtmp.go
+++ b/router/rtmp.go
@@ -26,44 +26,131 @@ import (
"gitea.drjosh.dev/josh/jrouter/atalk/rtmp"
"gitea.drjosh.dev/josh/jrouter/status"
- "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"
)
// RTMPMachine implements RTMP on an AppleTalk network attached to the router.
-type RTMPMachine struct {
- AARP *AARPMachine
- Config *Config
- PcapHandle *pcap.Handle
- RoutingTable *RoutingTable
+func (port *EtherTalkPort) HandleRTMP(ctx context.Context, pkt *ddp.ExtPacket) error {
+ switch pkt.Proto {
+ case ddp.ProtoRTMPReq:
+ // I can answer RTMP requests!
+ req, err := rtmp.UnmarshalRequestPacket(pkt.Data)
+ if err != nil {
+ return fmt.Errorf("unmarshal Request packet: %w", err)
+ }
+
+ switch req.Function {
+ case rtmp.FunctionRequest:
+ // Respond with RTMP Response
+ respPkt := &rtmp.ResponsePacket{
+ SenderAddr: port.MyAddr,
+ Extended: true,
+ RangeStart: port.NetStart,
+ RangeEnd: port.NetEnd,
+ }
+ respPktRaw, err := respPkt.Marshal()
+ if err != nil {
+ return fmt.Errorf("marshal RTMP Response packet: %w", err)
+ }
+ ddpPkt := &ddp.ExtPacket{
+ ExtHeader: ddp.ExtHeader{
+ Size: uint16(len(respPktRaw)) + atalk.DDPExtHeaderSize,
+ Cksum: 0,
+ DstNet: pkt.SrcNet,
+ DstNode: pkt.SrcNode,
+ DstSocket: 1, // the RTMP socket
+ SrcNet: port.MyAddr.Network,
+ SrcNode: port.MyAddr.Node,
+ SrcSocket: 1, // the RTMP socket
+ Proto: ddp.ProtoRTMPResp,
+ },
+ Data: respPktRaw,
+ }
+
+ if err := port.Router.Output(ctx, ddpPkt); err != nil {
+ return fmt.Errorf("send Response: %w", err)
+ }
+
+ case rtmp.FunctionRDRSplitHorizon, rtmp.FunctionRDRComplete:
+ // Like the Data broadcast, but solicited by a request (RDR).
+ splitHorizon := req.Function == rtmp.FunctionRDRSplitHorizon
+ for _, dataPkt := range port.rtmpDataPackets(splitHorizon) {
+ dataPktRaw, err := dataPkt.Marshal()
+ if err != nil {
+ return fmt.Errorf("marshal RTMP Data packet: %w", err)
+ }
+
+ ddpPkt := &ddp.ExtPacket{
+ ExtHeader: ddp.ExtHeader{
+ Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize,
+ Cksum: 0,
+ DstNet: pkt.SrcNet,
+ DstNode: pkt.SrcNode,
+ DstSocket: 1, // the RTMP socket
+ SrcNet: port.MyAddr.Network,
+ SrcNode: port.MyAddr.Node,
+ SrcSocket: 1, // the RTMP socket
+ Proto: ddp.ProtoRTMPResp,
+ },
+ Data: dataPktRaw,
+ }
+
+ if err := port.Router.Output(ctx, ddpPkt); err != nil {
+ return fmt.Errorf("send Data: %w", err)
+ }
+ }
+
+ case rtmp.FunctionLoopProbe:
+ log.Print("RTMP: TODO: handle Loop Probes")
+ return nil
+ }
+
+ case ddp.ProtoRTMPResp:
+ // It's a peer router on the AppleTalk network!
+ log.Print("RTMP: Got Response or Data")
+ dataPkt, err := rtmp.UnmarshalDataPacket(pkt.Data)
+ if err != nil {
+ log.Printf("RTMP: Couldn't unmarshal RTMP Data packet: %v", err)
+ break
+ }
+ peer := &EtherTalkPeer{
+ Port: port,
+ PeerAddr: dataPkt.RouterAddr,
+ }
+
+ for _, rt := range dataPkt.NetworkTuples {
+ if err := port.Router.RouteTable.UpsertEtherTalkRoute(peer, rt.Extended, rt.RangeStart, rt.RangeEnd, rt.Distance+1); err != nil {
+ log.Printf("RTMP: Couldn't upsert EtherTalk route: %v", err)
+ }
+ }
+
+ default:
+ log.Printf("RTMP: invalid DDP type %d on socket 1", pkt.Proto)
+ }
+
+ return nil
}
-// Run executes the machine.
-func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket) error {
- ctx, setStatus, done := status.AddSimpleItem(ctx, "RTMP")
- defer done()
+// RunRTMP makes periodic RTMP Data broadcasts on this port.
+func (port *EtherTalkPort) RunRTMP(ctx context.Context) (err error) {
+ ctx, setStatus, _ := status.AddSimpleItem(ctx, "RTMP")
+ defer func() {
+ setStatus(fmt.Sprintf("Run loop stopped! Return: %v", err))
+ }()
setStatus("Awaiting DDP address assignment")
// Await local address assignment before doing anything
- <-m.AARP.Assigned()
- myAddr, ok := m.AARP.Address()
- if !ok {
- return fmt.Errorf("AARP machine closed Assigned channel but Address is not valid")
- }
+ <-port.AARPMachine.Assigned()
setStatus("Initial RTMP Data broadcast")
// Initial broadcast
- if err := m.broadcastData(myAddr); err != nil {
+ if err := port.broadcastRTMPData(); err != nil {
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
}
- setStatus("Packet loop")
+ setStatus("Starting broadcast loop")
bcastTicker := time.NewTicker(10 * time.Second)
defer bcastTicker.Stop()
@@ -74,140 +161,18 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
return ctx.Err()
case <-bcastTicker.C:
- if err := m.broadcastData(myAddr); err != nil {
- log.Printf("RTMP: Couldn't broadcast Data: %v", err)
+ setStatus("Broadcasting RTMP Data")
+ if err := port.broadcastRTMPData(); err != nil {
+ st := fmt.Sprintf("Couldn't broadcast Data: %v", err)
+ setStatus(st)
+ log.Print(st)
}
-
- case pkt := <-incomingCh:
- switch pkt.Proto {
- case ddp.ProtoRTMPReq:
- // I can answer RTMP requests!
- req, err := rtmp.UnmarshalRequestPacket(pkt.Data)
- if err != nil {
- log.Printf("RTMP: Couldn't unmarshal Request packet: %v", err)
- }
-
- // should be in the cache...
- theirHWAddr, err := m.AARP.Resolve(ctx, ddp.Addr{Network: pkt.SrcNet, Node: pkt.SrcNode})
- if err != nil {
- log.Printf("RTMP: Couldn't resolve %d.%d to a hardware address: %v", pkt.SrcNet, pkt.SrcNode, err)
- continue
- }
-
- switch req.Function {
- case rtmp.FunctionRequest:
- // Respond with RTMP Response
- respPkt := &rtmp.ResponsePacket{
- SenderAddr: myAddr.Proto,
- Extended: true,
- RangeStart: m.Config.EtherTalk.NetStart,
- RangeEnd: m.Config.EtherTalk.NetEnd,
- }
- respPktRaw, err := respPkt.Marshal()
- if err != nil {
- log.Printf("RTMP: Couldn't marshal RTMP Response packet: %v", err)
- continue
- }
- ddpPkt := &ddp.ExtPacket{
- ExtHeader: ddp.ExtHeader{
- Size: uint16(len(respPktRaw)) + atalk.DDPExtHeaderSize,
- Cksum: 0,
- DstNet: pkt.SrcNet,
- DstNode: pkt.SrcNode,
- DstSocket: 1, // the RTMP socket
- SrcNet: myAddr.Proto.Network,
- SrcNode: myAddr.Proto.Node,
- SrcSocket: 1, // the RTMP socket
- Proto: ddp.ProtoRTMPResp,
- },
- Data: respPktRaw,
- }
-
- if err := m.send(myAddr.Hardware, theirHWAddr, ddpPkt); err != nil {
- log.Printf("RTMP: Couldn't send Data broadcast: %v", err)
- }
-
- case rtmp.FunctionRDRSplitHorizon, rtmp.FunctionRDRComplete:
- // Like the Data broadcast, but solicited by a request (RDR).
- // TODO: handle split-horizon processing
- for _, dataPkt := range m.dataPackets(myAddr.Proto) {
- dataPktRaw, err := dataPkt.Marshal()
- if err != nil {
- log.Printf("RTMP: Couldn't marshal Data packet: %v", err)
- break
- }
-
- ddpPkt := &ddp.ExtPacket{
- ExtHeader: ddp.ExtHeader{
- Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize,
- Cksum: 0,
- DstNet: pkt.SrcNet,
- DstNode: pkt.SrcNode,
- DstSocket: 1, // the RTMP socket
- SrcNet: myAddr.Proto.Network,
- SrcNode: myAddr.Proto.Node,
- SrcSocket: 1, // the RTMP socket
- Proto: ddp.ProtoRTMPResp,
- },
- Data: dataPktRaw,
- }
-
- if err := m.send(myAddr.Hardware, theirHWAddr, ddpPkt); err != nil {
- log.Printf("RTMP: Couldn't send Data response: %v", err)
- break
- }
- }
-
- case rtmp.FunctionLoopProbe:
- log.Print("RTMP: TODO: handle Loop Probes")
-
- }
-
- case ddp.ProtoRTMPResp:
- // It's a peer router on the AppleTalk network!
- log.Print("RTMP: Got Response or Data")
- dataPkt, err := rtmp.UnmarshalDataPacket(pkt.Data)
- if err != nil {
- log.Printf("RTMP: Couldn't unmarshal RTMP Data packet: %v", err)
- break
- }
- peer := &EtherTalkPeer{
- PcapHandle: m.PcapHandle,
- MyHWAddr: m.AARP.myAddr.Hardware,
- AARP: m.AARP,
- PeerAddr: dataPkt.RouterAddr,
- }
-
- for _, rt := range dataPkt.NetworkTuples {
- if err := m.RoutingTable.UpsertEthRoute(peer, rt.Extended, rt.RangeStart, rt.RangeEnd, rt.Distance+1); err != nil {
- log.Printf("RTMP: Couldn't upsert EtherTalk route: %v", err)
- }
- }
-
- default:
- log.Printf("RTMP: invalid DDP type %d on socket 1", pkt.Proto)
- }
-
}
}
}
-func (m *RTMPMachine) send(src, dst ethernet.Addr, ddpPkt *ddp.ExtPacket) error {
- ethFrame, err := ethertalk.AppleTalk(src, *ddpPkt)
- if err != nil {
- return err
- }
- ethFrame.Dst = dst
-
- ethFrameRaw, err := ethertalk.Marshal(*ethFrame)
- if err != nil {
- return err
- }
- return m.PcapHandle.WritePacketData(ethFrameRaw)
-}
-
-func (m *RTMPMachine) broadcastData(myAddr aarp.AddrPair) error {
- for _, dataPkt := range m.dataPackets(myAddr.Proto) {
+func (port *EtherTalkPort) broadcastRTMPData() error {
+ for _, dataPkt := range port.rtmpDataPackets(true) {
dataPktRaw, err := dataPkt.Marshal()
if err != nil {
return fmt.Errorf("marshal Data packet: %v", err)
@@ -217,29 +182,40 @@ func (m *RTMPMachine) broadcastData(myAddr aarp.AddrPair) error {
ExtHeader: ddp.ExtHeader{
Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0,
- DstNet: 0, // this network
- DstNode: 0xff, // broadcast packet
- DstSocket: 1, // the RTMP socket
- SrcNet: myAddr.Proto.Network,
- SrcNode: myAddr.Proto.Node,
+ DstNet: 0x0000, // this network
+ DstNode: 0xff, // broadcast packet
+ DstSocket: 1, // the RTMP socket
+ SrcNet: port.MyAddr.Network,
+ SrcNode: port.MyAddr.Node,
SrcSocket: 1, // the RTMP socket
Proto: ddp.ProtoRTMPResp,
},
Data: dataPktRaw,
}
- if err := m.send(myAddr.Hardware, ethertalk.AppleTalkBroadcast, ddpPkt); err != nil {
+ if err := port.Broadcast(ddpPkt); err != nil {
return err
}
}
return nil
}
-func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket {
+func (port *EtherTalkPort) rtmpDataPackets(splitHorizon bool) []*rtmp.DataPacket {
// Build up a slice of routing tuples.
- routes := m.RoutingTable.ValidRoutes()
+ routes := port.Router.RouteTable.ValidRoutes()
tuples := make([]rtmp.NetworkTuple, 0, len(routes))
for _, rt := range routes {
+ if rt.EtherTalkDirect == port {
+ // If the route is actually a direct connection to this port,
+ // don't include it.
+ // (It's manually set as the first tuple anyway.)
+ continue
+ }
+ if splitHorizon && rt.EtherTalkPeer != nil && rt.EtherTalkPeer.Port == port {
+ // If the route is through a peer accessible on this port, don't
+ // include it.
+ continue
+ }
tuples = append(tuples, rtmp.NetworkTuple{
Extended: rt.Extended,
RangeStart: rt.NetStart,
@@ -253,8 +229,8 @@ func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket {
// TODO: support non-extended local networks (LocalTalk)
first := rtmp.NetworkTuple{
Extended: true,
- RangeStart: m.Config.EtherTalk.NetStart,
- RangeEnd: m.Config.EtherTalk.NetEnd,
+ RangeStart: port.NetStart,
+ RangeEnd: port.NetEnd,
Distance: 0,
}
@@ -274,7 +250,7 @@ func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket {
rem = rem[len(chunk)-1:]
packets = append(packets, &rtmp.DataPacket{
- RouterAddr: myAddr,
+ RouterAddr: port.MyAddr,
Extended: true,
NetworkTuples: chunk,
})
diff --git a/router/zip.go b/router/zip.go
index ded821f..669e1f4 100644
--- a/router/zip.go
+++ b/router/zip.go
@@ -20,266 +20,300 @@ 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"
- "github.com/sfiera/multitalk/pkg/ethertalk"
)
-func (rtr *Router) HandleZIP(ctx context.Context, srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) error {
+func (port *EtherTalkPort) HandleZIP(ctx context.Context, ddpkt *ddp.ExtPacket) error {
switch ddpkt.Proto {
case ddp.ProtoATP:
- 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
- }
- 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 = rtr.ZoneTable.AllNames()
-
- case zip.FunctionGetLocalZones:
- resp.Zones = rtr.ZoneTable.LocalNames()
-
- case zip.FunctionGetMyZone:
- resp.Zones = []string{rtr.Config.EtherTalk.ZoneName}
- }
-
- // 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"
- 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: rtr.MyDDPAddr.Network,
- SrcNode: rtr.MyDDPAddr.Node,
- SrcSocket: 6,
- Proto: ddp.ProtoATP,
- },
- Data: ddpBody,
- }
- return rtr.sendEtherTalkDDP(srcHWAddr, respDDP)
-
- case *atp.TResp:
- return fmt.Errorf("TODO: support handling ZIP ATP replies?")
-
- default:
- return fmt.Errorf("unsupported ATP packet type %T for ZIP", atpkt)
- }
+ return port.handleZIPATP(ctx, ddpkt)
case ddp.ProtoZIP:
- zipkt, err := zip.UnmarshalPacket(ddpkt.Data)
- if err != nil {
- return err
- }
-
- switch zipkt := zipkt.(type) {
- case *zip.QueryPacket:
- log.Printf("ZIP: Got Query for networks %v", zipkt.Networks)
- networks := rtr.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: rtr.MyDDPAddr.Network,
- SrcNode: rtr.MyDDPAddr.Node,
- SrcSocket: 6,
- Proto: ddp.ProtoZIP,
- },
- Data: respRaw,
- }
- return rtr.sendEtherTalkDDP(srcHWAddr, 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
-
- case *zip.GetNetInfoPacket:
- log.Printf("ZIP: Got GetNetInfo for zone %q", zipkt.ZoneName)
-
- // Only running a network with one zone for now.
- resp := &zip.GetNetInfoReplyPacket{
- ZoneInvalid: zipkt.ZoneName != rtr.Config.EtherTalk.ZoneName,
- UseBroadcast: false,
- OnlyOneZone: true,
- NetStart: rtr.Config.EtherTalk.NetStart,
- NetEnd: rtr.Config.EtherTalk.NetEnd,
- ZoneName: zipkt.ZoneName, // has to match request
- MulticastAddr: atalk.MulticastAddr(rtr.Config.EtherTalk.ZoneName),
- DefaultZoneName: rtr.Config.EtherTalk.ZoneName,
- }
- 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: rtr.MyDDPAddr.Network,
- SrcNode: rtr.MyDDPAddr.Node,
- SrcSocket: 6,
- Proto: ddp.ProtoZIP,
- },
- Data: respRaw,
- }
- if ddpkt.DstNet == 0x0000 {
- outDDP.DstNet = 0x0000
- }
- if ddpkt.DstNode == 0xFF {
- outDDP.DstNode = 0xFF
- }
-
- // If it's a broadcast packet, broadcast it. Otherwise don't
- dstEth := ethertalk.AppleTalkBroadcast
- if outDDP.DstNode != 0xFF {
- dstEth = srcHWAddr
- }
-
- return rtr.sendEtherTalkDDP(dstEth, outDDP)
-
- default:
- return fmt.Errorf("TODO: handle type %T", zipkt)
- }
+ 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.Send(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 zoneValid if the zone name is available on this network.
+ zoneValid := slices.Contains(port.AvailableZones, zipkt.ZoneName)
+
+ // The multicast address we return depends on the validity of the zone
+ // name.
+ var mcastAddr ethernet.Addr
+ if zoneValid {
+ mcastAddr = atalk.MulticastAddr(zipkt.ZoneName)
+ } else {
+ mcastAddr = atalk.MulticastAddr(port.DefaultZoneName)
+ }
+
+ resp := &zip.GetNetInfoReplyPacket{
+ ZoneInvalid: !zoneValid,
+ 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 !zoneValid {
+ 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.Send(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.Send(ctx, respDDP)
+}
diff --git a/router/zones.go b/router/zones.go
index c05f6ff..f1d0d74 100644
--- a/router/zones.go
+++ b/router/zones.go
@@ -29,10 +29,10 @@ import (
//const maxZoneAge = 10 * time.Minute // TODO: confirm
type Zone struct {
- Network ddp.Network
- Name string
- Local bool
- LastSeen time.Time
+ Network ddp.Network
+ Name string
+ LocalPort *EtherTalkPort // nil if remote (local to another router)
+ LastSeen time.Time
}
func (z Zone) LastSeenAgo() string {
@@ -68,21 +68,21 @@ func (zt *ZoneTable) Dump() []Zone {
return zs
}
-func (zt *ZoneTable) Upsert(network ddp.Network, name string, local bool) {
+func (zt *ZoneTable) Upsert(network ddp.Network, name string, localPort *EtherTalkPort) {
zt.mu.Lock()
defer zt.mu.Unlock()
key := zoneKey{network, name}
z := zt.zones[key]
if z != nil {
- z.Local = local
+ z.LocalPort = localPort
z.LastSeen = time.Now()
return
}
zt.zones[key] = &Zone{
- Network: network,
- Name: name,
- Local: local,
- LastSeen: time.Now(),
+ Network: network,
+ Name: name,
+ LocalPort: localPort,
+ LastSeen: time.Now(),
}
}
@@ -116,29 +116,29 @@ func (zt *ZoneTable) LookupName(name string) []*Zone {
return zs
}
-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)
+// 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 != nil {
+// continue
+// }
+// if _, s := seen[z.Name]; s {
+// continue
+// }
+// seen[z.Name] = struct{}{}
+// zs = append(zs, z.Name)
- }
- zt.mu.Unlock()
+// }
+// zt.mu.Unlock()
- sort.Strings(zs)
- return zs
-}
+// sort.Strings(zs)
+// return zs
+// }
func (zt *ZoneTable) AllNames() []string {
zt.mu.Lock()