Merge pull request 'Multi-port refactor' (#1) from multi-port-refactor into main

Reviewed-on: #1
This commit is contained in:
Josh Deprez 2024-05-04 17:06:20 +10:00
commit 8ce8f52776
14 changed files with 961 additions and 869 deletions

View file

@ -101,7 +101,7 @@ func Unmarshal(data []byte) (*Packet, error) {
t := Tuple{ t := Tuple{
Network: ddp.Network(binary.BigEndian.Uint16(data[:2])), Network: ddp.Network(binary.BigEndian.Uint16(data[:2])),
Node: ddp.Node(data[2]), Node: ddp.Node(data[2]),
Socket: data[3], Socket: ddp.Socket(data[3]),
Enumerator: data[4], Enumerator: data[4],
} }
data = data[5:] data = data[5:]
@ -128,7 +128,7 @@ func Unmarshal(data []byte) (*Packet, error) {
type Tuple struct { type Tuple struct {
Network ddp.Network Network ddp.Network
Node ddp.Node Node ddp.Node
Socket uint8 Socket ddp.Socket
Enumerator uint8 Enumerator uint8
Object string // length-prefixed Object string // length-prefixed
Type string // length-prefixed Type string // length-prefixed
@ -147,7 +147,7 @@ func (t *Tuple) writeTo(b *bytes.Buffer) error {
} }
write16(b, t.Network) write16(b, t.Network)
b.WriteByte(byte(t.Node)) b.WriteByte(byte(t.Node))
b.WriteByte(t.Socket) b.WriteByte(byte(t.Socket))
b.WriteByte(t.Enumerator) b.WriteByte(t.Enumerator)
b.WriteByte(byte(len(t.Object))) b.WriteByte(byte(len(t.Object)))
b.WriteString(t.Object) b.WriteString(t.Object)

275
main.go
View file

@ -23,7 +23,6 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"math/rand/v2" "math/rand/v2"
"net" "net"
@ -44,7 +43,6 @@ import (
"github.com/google/gopacket/pcap" "github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet" "github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
) )
const routingTableTemplate = ` const routingTableTemplate = `
@ -63,7 +61,17 @@ const routingTableTemplate = `
<td>{{if $route.Extended}}{{else}}{{end}}</td> <td>{{if $route.Extended}}{{else}}{{end}}</td>
<td>{{$route.Distance}}</td> <td>{{$route.Distance}}</td>
<td>{{$route.LastSeenAgo}}</td> <td>{{$route.LastSeenAgo}}</td>
<td>{{if $route.AURPPeer}}{{$route.AURPPeer.RemoteAddr}}{{else if $route.EtherTalkPeer}}{{$route.EtherTalkPeer.PeerAddr.Network}}.{{$route.EtherTalkPeer.PeerAddr.Node}}{{else}}-{{end}}</td> <td>
{{- with $route.AURPPeer -}}
{{.RemoteAddr}}
{{- end -}}
{{- with $route.EtherTalkPeer -}}
{{.Port.Device}} {{.PeerAddr.Network}}.{{.PeerAddr.Node}}
{{- end -}}
{{- with $route.EtherTalkDirect -}}
{{.Device}} {{.NetStart}}-{{.NetEnd}}
{{- end -}}
</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
@ -75,7 +83,7 @@ const zoneTableTemplate = `
<thead><tr> <thead><tr>
<th>Network</th> <th>Network</th>
<th>Name</th> <th>Name</th>
<th>Local</th> <th>Local Port</th>
<th>Last seen</th> <th>Last seen</th>
</tr></thead> </tr></thead>
<tbody> <tbody>
@ -83,7 +91,7 @@ const zoneTableTemplate = `
<tr> <tr>
<td>{{$zone.Network}}</td> <td>{{$zone.Network}}</td>
<td>{{$zone.Name}}</td> <td>{{$zone.Name}}</td>
<td>{{if $zone.Local}}{{else}}{{end}}</td> <td>{{with $zone.LocalPort}}{{.Device}}{{else}}-{{end}}</td>
<td>{{$zone.LastSeenAgo}}</td> <td>{{$zone.LastSeenAgo}}</td>
</tr> </tr>
{{end}} {{end}}
@ -159,10 +167,10 @@ func main() {
ln, err := net.ListenUDP("udp4", &net.UDPAddr{Port: int(cfg.ListenPort)}) ln, err := net.ListenUDP("udp4", &net.UDPAddr{Port: int(cfg.ListenPort)})
if err != nil { 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() 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") log.Println("Press ^C or send SIGINT to stop the router gracefully")
cctx, cancel := context.WithCancel(context.Background()) cctx, cancel := context.WithCancel(context.Background())
@ -203,7 +211,7 @@ func main() {
defer pcapHandle.Close() defer pcapHandle.Close()
// -------------------------------- Tables -------------------------------- // -------------------------------- Tables --------------------------------
routes := router.NewRoutingTable() routes := router.NewRouteTable()
status.AddItem(ctx, "Routing table", routingTableTemplate, func(context.Context) (any, error) { status.AddItem(ctx, "Routing table", routingTableTemplate, func(context.Context) (any, error) {
rs := routes.Dump() rs := routes.Dump()
slices.SortFunc(rs, func(ra, rb router.Route) int { slices.SortFunc(rs, func(ra, rb router.Route) int {
@ -213,7 +221,6 @@ func main() {
}) })
zones := router.NewZoneTable() zones := router.NewZoneTable()
zones.Upsert(cfg.EtherTalk.NetStart, cfg.EtherTalk.ZoneName, true)
status.AddItem(ctx, "Zone table", zoneTableTemplate, func(context.Context) (any, error) { status.AddItem(ctx, "Zone table", zoneTableTemplate, func(context.Context) (any, error) {
zs := zones.Dump() zs := zones.Dump()
slices.SortFunc(zs, func(za, zb router.Zone) int { slices.SortFunc(zs, func(za, zb router.Zone) int {
@ -331,174 +338,46 @@ func main() {
// --------------------------------- AARP --------------------------------- // --------------------------------- AARP ---------------------------------
aarpMachine := router.NewAARPMachine(cfg, pcapHandle, myHWAddr) aarpMachine := router.NewAARPMachine(cfg, pcapHandle, myHWAddr)
aarpCh := make(chan *ethertalk.Packet, 1024) go aarpMachine.Run(ctx)
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)
// -------------------------------- Router -------------------------------- // -------------------------------- Router --------------------------------
rooter := &router.Router{ rooter := &router.Router{
Config: cfg, Config: cfg,
PcapHandle: pcapHandle,
MyHWAddr: myHWAddr,
// MyDDPAddr: ...,
AARPMachine: aarpMachine,
RouteTable: routes, RouteTable: routes,
ZoneTable: zones, 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 ---------------------- // ---------------------- Raw AppleTalk/AARP inbound ----------------------
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
ctx, setStatus, done := status.AddSimpleItem(ctx, "EtherTalk inbound") ctx, setStatus, _ := status.AddSimpleItem(ctx, "EtherTalk inbound")
defer done() defer setStatus("EtherTalk Serve goroutine exited!")
setStatus(fmt.Sprintf("Listening on %s", cfg.EtherTalk.Device)) setStatus(fmt.Sprintf("Listening on %s", cfg.EtherTalk.Device))
for { etherTalkPort.Serve(ctx)
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)
}
}
}() }()
// ----------------------------- AURP inbound ----------------------------- // ----------------------------- AURP inbound -----------------------------
@ -593,79 +472,37 @@ func main() {
log.Printf("AURP: Couldn't unmarshal encapsulated DDP packet: %v", err) log.Printf("AURP: Couldn't unmarshal encapsulated DDP packet: %v", err)
continue continue
} }
log.Printf("DDP/AURP: Got %d.%d.%d -> %d.%d.%d proto %d data len %d", // log.Printf("DDP/AURP: Got %d.%d.%d -> %d.%d.%d proto %d data len %d",
ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket, // ddpkt.SrcNet, ddpkt.SrcNode, ddpkt.SrcSocket,
ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket, // ddpkt.DstNet, ddpkt.DstNode, ddpkt.DstSocket,
ddpkt.Proto, len(ddpkt.Data)) // ddpkt.Proto, len(ddpkt.Data))
// Route the packet // Is it addressed to me?
var localPort *router.EtherTalkPort
// Check and adjust the Hop Count for _, port := range rooter.Ports {
// Note the ddp package doesn't make this simple if ddpkt.DstNet >= port.NetStart && ddpkt.DstNet <= port.NetEnd {
hopCount := (ddpkt.Size & 0x3C00) >> 10 localPort = port
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)
break 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 { if ddpkt.DstNode == 0 && localPort != nil { // Node 0 = any router for the network = me
log.Printf("DDP/AURP: Couldn't forward packet to AURP peer: %v", err) // Is it NBP? FwdReq needs translating.
}
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.DstSocket != 2 { if ddpkt.DstSocket != 2 {
// Something else?? TODO // Something else?? TODO
log.Printf("DDP/AURP: I don't have anything 'listening' on socket %d", ddpkt.DstSocket) log.Printf("DDP/AURP: I don't have anything 'listening' on socket %d", ddpkt.DstSocket)
continue continue
} }
// It's NBP // It's NBP, specifically it should be a FwdReq
if err := rooter.HandleNBPInAURP(pr, ddpkt); err != nil { if err := rooter.HandleNBPFromAURP(ctx, ddpkt); err != nil {
log.Printf("NBP/DDP/AURP: %v", err) log.Printf("NBP/DDP/AURP: %v", err)
} }
continue continue
} }
// Note: resolving AARP can block // Route the packet!
if err := rooter.SendEtherTalkDDP(ctx, ddpkt); err != nil { if err := rooter.Forward(ctx, ddpkt); err != nil {
log.Printf("DDP/AURP: couldn't send Ethertalk out: %v", err) log.Printf("DDP/AURP: Couldn't route packet: %v", err)
} }
continue
default: default:
log.Printf("AURP: Got unknown packet type %v", dh.PacketType) log.Printf("AURP: Got unknown packet type %v", dh.PacketType)

View file

@ -73,6 +73,8 @@ type AARPMachine struct {
cfg *Config cfg *Config
pcapHandle *pcap.Handle pcapHandle *pcap.Handle
incomingCh chan *ethertalk.Packet
// The Run goroutine is responsible for all writes to myAddr.Proto and // 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 // probes, so this mutex is not used to enforce a single writer, only
// consistent reads // consistent reads
@ -90,6 +92,7 @@ func NewAARPMachine(cfg *Config, pcapHandle *pcap.Handle, myHWAddr ethernet.Addr
addressMappingTable: new(addressMappingTable), addressMappingTable: new(addressMappingTable),
cfg: cfg, cfg: cfg,
pcapHandle: pcapHandle, pcapHandle: pcapHandle,
incomingCh: make(chan *ethertalk.Packet, 1024), // arbitrary capacity
myAddr: aarp.AddrPair{ myAddr: aarp.AddrPair{
Hardware: myHWAddr, 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 // Address returns the address of this node, and reports if the address is valid
// (i.e. not tentative). // (i.e. not tentative).
func (a *AARPMachine) Address() (aarp.AddrPair, bool) { func (a *AARPMachine) Address() (aarp.AddrPair, bool) {
@ -123,7 +134,7 @@ func (a *AARPMachine) status(ctx context.Context) (any, error) {
} }
// Run executes the machine. // 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) ctx, done := status.AddItem(ctx, "AARP", aarpStatusTemplate, a.status)
defer done() 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) log.Printf("Couldn't broadcast a Probe: %v", err)
} }
case ethFrame, ok := <-incomingCh: case ethFrame, ok := <-a.incomingCh:
if !ok { if !ok {
incomingCh = nil a.incomingCh = nil
} }
var aapkt aarp.Packet var aapkt aarp.Packet

View file

@ -17,14 +17,14 @@
package router package router
import ( import (
"context"
"fmt" "fmt"
"gitea.drjosh.dev/josh/jrouter/atalk/aep" "gitea.drjosh.dev/josh/jrouter/atalk/aep"
"github.com/sfiera/multitalk/pkg/ddp" "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 { if ddpkt.Proto != ddp.ProtoAEP {
return fmt.Errorf("invalid DDP type %d on socket 4", ddpkt.Proto) 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.DstSocket, ddpkt.SrcSocket = ddpkt.SrcSocket, ddpkt.DstSocket
ddpkt.Data[0] = byte(aep.EchoReply) ddpkt.Data[0] = byte(aep.EchoReply)
return rtr.sendEtherTalkDDP(src, ddpkt) return rtr.Output(ctx, ddpkt)
default: default:
return fmt.Errorf("invalid AEP function %d", ep.Function) return fmt.Errorf("invalid AEP function %d", ep.Function)

View file

@ -17,16 +17,17 @@
package router package router
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"slices"
"gitea.drjosh.dev/josh/jrouter/atalk" "gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/nbp" "gitea.drjosh.dev/josh/jrouter/atalk/nbp"
"github.com/sfiera/multitalk/pkg/ddp" "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 { if ddpkt.Proto != ddp.ProtoNBP {
return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto) return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto)
} }
@ -41,23 +42,44 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
switch nbpkt.Function { switch nbpkt.Function {
case nbp.FunctionLkUp: case nbp.FunctionLkUp:
// when in AppleTalk, do as Apple Internet Router does... // 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 { if err != nil || outDDP == nil {
return err return err
} }
log.Print("NBP: Replying to LkUp with LkUp-Reply for myself") 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: case nbp.FunctionBrRq:
return port.handleNBPBrRq(ctx, ddpkt, nbpkt)
default:
return fmt.Errorf("TODO: handle function %v", nbpkt.Function)
}
}
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.
// There must be 1! // There must be 1!
tuple := &nbpkt.Tuples[0] tuple := &nbpkt.Tuples[0]
zones := rtr.ZoneTable.LookupName(tuple.Zone) // This logic would be required on a non-extended network:
// if tuple.Zone == "" || tuple.Zone == "*" {
// tuple.Zone = port.DefaultZoneName
// }
zones := port.Router.ZoneTable.LookupName(tuple.Zone)
for _, z := range zones { for _, z := range zones {
if z.Local { if outPort := z.LocalPort; outPort != nil {
// If it's for the local zone, translate it to a LkUp and broadcast it back // If it's for a local zone, translate it to a LkUp and broadcast
// out the EtherTalk port. // out the corresponding EtherTalk port.
// "Note: On an internet, nodes on extended networks performing lookups in // "Note: On an internet, nodes on extended networks performing lookups in
// their own zone must replace a zone name of asterisk (*) with their actual // 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 // zone name before sending the packet to A-ROUTER. All nodes performing
@ -74,9 +96,9 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
ExtHeader: ddp.ExtHeader{ ExtHeader: ddp.ExtHeader{
Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)), Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)),
Cksum: 0, Cksum: 0,
SrcNet: ddpkt.SrcNet, SrcNet: port.MyAddr.Network,
SrcNode: ddpkt.SrcNode, SrcNode: port.MyAddr.Node,
SrcSocket: ddpkt.SrcSocket, SrcSocket: 2,
DstNet: 0x0000, // Local network broadcast DstNet: 0x0000, // Local network broadcast
DstNode: 0xFF, // Broadcast node address within the dest network DstNode: 0xFF, // Broadcast node address within the dest network
DstSocket: 2, DstSocket: 2,
@ -86,44 +108,39 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
} }
log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple) log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple)
if err := rtr.ZoneMulticastEtherTalkDDP(tuple.Zone, &outDDP); err != nil { if err := outPort.ZoneMulticast(tuple.Zone, &outDDP); err != nil {
return err return err
} }
// But also...if we match the query, reply as though it was a LkUp // But also...if we match the query, reply as though it was a LkUp
outDDP2, err := rtr.helloWorldThisIsMe(ddpkt, nbpkt.NBPID, tuple) // This uses the *input* port information.
outDDP2, err := port.helloWorldThisIsMe(nbpkt.NBPID, tuple)
if err != nil { if err != nil {
return err return err
} }
if outDDP2 == nil { if outDDP2 == nil {
continue continue
} }
log.Print("NBP: Replying to BrRq with LkUp-Reply for myself") log.Print("NBP: Replying to BrRq directly with LkUp-Reply for myself")
if err := rtr.sendEtherTalkDDP(srcHWAddr, outDDP2); err != nil { // 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 return err
} }
continue continue
} }
route := rtr.RouteTable.LookupRoute(z.Network) // The zone table row is *not* for a local network.
if route == nil { // Translate it into a FwdReq and route that to the routers that do have
return fmt.Errorf("no route for network %d", z.Network) // that zone as a local 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 nbpkt.Function = nbp.FunctionFwdReq
nbpRaw, err := nbpkt.Marshal() nbpRaw, err := nbpkt.Marshal()
if err != nil { if err != nil {
return fmt.Errorf("couldn't marshal FwdReq: %v", err) return fmt.Errorf("couldn't marshal FwdReq: %v", err)
} }
outDDP := ddp.ExtPacket{ outDDP := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{ ExtHeader: ddp.ExtHeader{
Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)), Size: atalk.DDPExtHeaderSize + uint16(len(nbpRaw)),
Cksum: 0, Cksum: 0,
@ -138,33 +155,67 @@ func (rtr *Router) HandleNBP(srcHWAddr ethernet.Addr, ddpkt *ddp.ExtPacket) erro
Data: nbpRaw, Data: nbpRaw,
} }
outDDPRaw, err := ddp.ExtMarshal(outDDP) if err := port.Router.Output(ctx, outDDP); err != nil {
if err != nil {
return err 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)
} }
}
default:
return fmt.Errorf("TODO: handle function %v", nbpkt.Function)
}
return nil 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 != "=" { if tuple.Object != "jrouter" && tuple.Object != "=" {
return nil, nil return nil, nil
} }
if tuple.Type != "AppleRouter" && tuple.Type != "=" { if tuple.Type != "AppleRouter" && tuple.Type != "=" {
return nil, nil 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 return nil, nil
} }
respPkt := &nbp.Packet{ respPkt := &nbp.Packet{
@ -172,13 +223,13 @@ func (rtr *Router) helloWorldThisIsMe(ddpkt *ddp.ExtPacket, nbpID uint8, tuple *
NBPID: nbpID, NBPID: nbpID,
Tuples: []nbp.Tuple{ Tuples: []nbp.Tuple{
{ {
Network: rtr.MyDDPAddr.Network, Network: port.MyAddr.Network,
Node: rtr.MyDDPAddr.Node, Node: port.MyAddr.Node,
Socket: 253, Socket: 253,
Enumerator: 0, Enumerator: 0,
Object: "jrouter", Object: "jrouter",
Type: "AppleRouter", 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 { if err != nil {
return nil, fmt.Errorf("couldn't marshal LkUp-Reply: %v", err) 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{ return &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{ ExtHeader: ddp.ExtHeader{
Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize, Size: uint16(len(respRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0, Cksum: 0,
DstNet: ddpkt.SrcNet, DstNet: tuple.Network,
DstNode: ddpkt.SrcNode, DstNode: tuple.Node,
DstSocket: ddpkt.SrcSocket, DstSocket: tuple.Socket,
SrcNet: rtr.MyDDPAddr.Network, SrcNet: port.MyAddr.Network,
SrcNode: rtr.MyDDPAddr.Node, SrcNode: port.MyAddr.Node,
SrcSocket: 2, SrcSocket: 2,
Proto: ddp.ProtoNBP, Proto: ddp.ProtoNBP,
}, },

View file

@ -17,14 +17,14 @@
package router package router
import ( import (
"context"
"fmt" "fmt"
"log"
"gitea.drjosh.dev/josh/jrouter/atalk/nbp" "gitea.drjosh.dev/josh/jrouter/atalk/nbp"
"github.com/sfiera/multitalk/pkg/ddp" "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 { if ddpkt.Proto != ddp.ProtoNBP {
return fmt.Errorf("invalid DDP type %d on socket 2", ddpkt.Proto) 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?? // It's something else??
return fmt.Errorf("can't handle %v", nbpkt.Function) return fmt.Errorf("can't handle %v", nbpkt.Function)
} }
return rtr.handleNBPFwdReq(ctx, ddpkt, nbpkt)
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
} }

View file

@ -115,7 +115,7 @@ type AURPPeer struct {
ReceiveCh chan aurp.Packet ReceiveCh chan aurp.Packet
// Routing table (the peer will add/remove/update routes) // Routing table (the peer will add/remove/update routes)
RoutingTable *RoutingTable RoutingTable *RouteTable
// Zone table (the peer will add/remove/update zones) // Zone table (the peer will add/remove/update zones)
ZoneTable *ZoneTable ZoneTable *ZoneTable
@ -125,6 +125,15 @@ type AURPPeer struct {
sstate SenderState 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 { func (p *AURPPeer) ReceiverState() ReceiverState {
p.mu.RLock() p.mu.RLock()
defer p.mu.RUnlock() defer p.mu.RUnlock()
@ -596,7 +605,7 @@ func (p *AURPPeer) Handle(ctx context.Context) error {
case *aurp.ZIRspPacket: case *aurp.ZIRspPacket:
log.Printf("AURP Peer: Learned about these zones: %v", pkt.Zones) log.Printf("AURP Peer: Learned about these zones: %v", pkt.Zones)
for _, zt := range 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: case *aurp.GDZLReqPacket:

View file

@ -19,29 +19,25 @@ package router
import ( import (
"context" "context"
"github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk" "github.com/sfiera/multitalk/pkg/ethertalk"
) )
// EtherTalkPeer holds data needed to exchange routes and zones with another // EtherTalkPeer holds data needed to forward packets to another router on the
// router on the EtherTalk network. // EtherTalk network.
type EtherTalkPeer struct { type EtherTalkPeer struct {
PcapHandle *pcap.Handle Port *EtherTalkPort
MyHWAddr ethernet.Addr
AARP *AARPMachine
PeerAddr ddp.Addr PeerAddr ddp.Addr
} }
// Forward forwards a DDP packet to the next router. // Forward forwards a DDP packet to the next router.
func (p *EtherTalkPeer) Forward(ctx context.Context, pkt *ddp.ExtPacket) error { func (p *EtherTalkPeer) Forward(ctx context.Context, pkt *ddp.ExtPacket) error {
// TODO: AARP resolution can block // 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 { if err != nil {
return err return err
} }
outFrame, err := ethertalk.AppleTalk(p.MyHWAddr, *pkt) outFrame, err := ethertalk.AppleTalk(p.Port.EthernetAddr, *pkt)
if err != nil { if err != nil {
return err return err
} }
@ -50,5 +46,5 @@ func (p *EtherTalkPeer) Forward(ctx context.Context, pkt *ddp.ExtPacket) error {
if err != nil { if err != nil {
return err return err
} }
return p.PcapHandle.WritePacketData(outFrameRaw) return p.Port.PcapHandle.WritePacketData(outFrameRaw)
} }

193
router/port.go Normal file
View file

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

View file

@ -35,8 +35,9 @@ type Route struct {
LastSeen time.Time LastSeen time.Time
// Exactly one of the following should be set // Exactly one of the following should be set
AURPPeer *AURPPeer AURPPeer *AURPPeer // Next hop is this peer router (over AURP)
EtherTalkPeer *EtherTalkPeer EtherTalkPeer *EtherTalkPeer // Next hop is this peer router (over EtherTalk)
EtherTalkDirect *EtherTalkPort // Directly connected to this network (via EtherTalk)
} }
func (r Route) LastSeenAgo() string { 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)) return fmt.Sprintf("%v ago", time.Since(r.LastSeen).Truncate(time.Millisecond))
} }
type RoutingTable struct { type RouteTable struct {
mu sync.Mutex mu sync.Mutex
routes map[*Route]struct{} routes map[*Route]struct{}
} }
func NewRoutingTable() *RoutingTable { func NewRouteTable() *RouteTable {
return &RoutingTable{ return &RouteTable{
routes: make(map[*Route]struct{}), 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() rt.mu.Lock()
defer rt.mu.Unlock() defer rt.mu.Unlock()
@ -68,7 +84,7 @@ func (rt *RoutingTable) Dump() []Route {
return table return table
} }
func (rt *RoutingTable) LookupRoute(network ddp.Network) *Route { func (rt *RouteTable) LookupRoute(network ddp.Network) *Route {
rt.mu.Lock() rt.mu.Lock()
defer rt.mu.Unlock() defer rt.mu.Unlock()
@ -92,7 +108,7 @@ func (rt *RoutingTable) LookupRoute(network ddp.Network) *Route {
return bestRoute return bestRoute
} }
func (rt *RoutingTable) DeleteAURPPeer(peer *AURPPeer) { func (rt *RouteTable) DeleteAURPPeer(peer *AURPPeer) {
rt.mu.Lock() rt.mu.Lock()
defer rt.mu.Unlock() 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() rt.mu.Lock()
defer rt.mu.Unlock() 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() rt.mu.Lock()
defer rt.mu.Unlock() 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 { if netStart > netEnd {
return fmt.Errorf("invalid network range [%d, %d]", 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 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 { if netStart > netEnd {
return fmt.Errorf("invalid network range [%d, %d]", 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 return nil
} }
func (rt *RoutingTable) ValidRoutes() []*Route { func (rt *RouteTable) ValidRoutes() []*Route {
rt.mu.Lock() rt.mu.Lock()
defer rt.mu.Unlock() defer rt.mu.Unlock()
valid := make([]*Route, 0, len(rt.routes)) valid := make([]*Route, 0, len(rt.routes))

View file

@ -18,53 +18,56 @@ package router
import ( import (
"context" "context"
"fmt"
"gitea.drjosh.dev/josh/jrouter/atalk"
"github.com/google/gopacket/pcap"
"github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
) )
type Router struct { type Router struct {
Config *Config Config *Config
PcapHandle *pcap.Handle RouteTable *RouteTable
MyHWAddr ethernet.Addr
MyDDPAddr ddp.Addr
AARPMachine *AARPMachine
RouteTable *RoutingTable
ZoneTable *ZoneTable ZoneTable *ZoneTable
Ports []*EtherTalkPort
} }
func (rtr *Router) SendEtherTalkDDP(ctx context.Context, pkt *ddp.ExtPacket) error { // Forward increments the hop count, then outputs the packet in the direction
dstEth := ethertalk.AppleTalkBroadcast // of the destination.
if pkt.DstNode != 0xFF { func (rtr *Router) Forward(ctx context.Context, ddpkt *ddp.ExtPacket) error {
de, err := rtr.AARPMachine.Resolve(ctx, ddp.Addr{Network: pkt.DstNet, Node: pkt.DstNode}) // Check and adjust the Hop Count
if err != nil { // Note the ddp package doesn't make this simple
return err hopCount := (ddpkt.Size & 0x3C00) >> 10
if hopCount >= 15 {
return fmt.Errorf("hop count exceeded (%d >= 15)", hopCount)
} }
dstEth = de hopCount++
} ddpkt.Size &^= 0x3C00
return rtr.sendEtherTalkDDP(dstEth, pkt) ddpkt.Size |= hopCount << 10
return rtr.Output(ctx, ddpkt)
} }
func (rtr *Router) BroadcastEtherTalkDDP(pkt *ddp.ExtPacket) error { // Output outputs the packet in the direction of the destination.
return rtr.sendEtherTalkDDP(ethertalk.AppleTalkBroadcast, pkt) // (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 { case route.AURPPeer != nil:
return rtr.sendEtherTalkDDP(atalk.MulticastAddr(zone), pkt) // 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 { case route.EtherTalkPeer != nil:
outFrame, err := ethertalk.AppleTalk(rtr.MyHWAddr, *pkt) // log.Printf("Forwarding to EtherTalk peer %v", route.EtherTalkPeer.PeerAddr)
if err != nil { // Note: resolving AARP can block
return err 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)
} }

View file

@ -26,87 +26,31 @@ import (
"gitea.drjosh.dev/josh/jrouter/atalk/rtmp" "gitea.drjosh.dev/josh/jrouter/atalk/rtmp"
"gitea.drjosh.dev/josh/jrouter/status" "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/ddp"
"github.com/sfiera/multitalk/pkg/ethernet"
"github.com/sfiera/multitalk/pkg/ethertalk"
) )
// RTMPMachine implements RTMP on an AppleTalk network attached to the router. // RTMPMachine implements RTMP on an AppleTalk network attached to the router.
type RTMPMachine struct { func (port *EtherTalkPort) HandleRTMP(ctx context.Context, pkt *ddp.ExtPacket) error {
AARP *AARPMachine
Config *Config
PcapHandle *pcap.Handle
RoutingTable *RoutingTable
}
// 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()
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")
}
setStatus("Initial RTMP Data broadcast")
// Initial broadcast
if err := m.broadcastData(myAddr); err != nil {
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
}
setStatus("Packet loop")
bcastTicker := time.NewTicker(10 * time.Second)
defer bcastTicker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-bcastTicker.C:
if err := m.broadcastData(myAddr); err != nil {
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
}
case pkt := <-incomingCh:
switch pkt.Proto { switch pkt.Proto {
case ddp.ProtoRTMPReq: case ddp.ProtoRTMPReq:
// I can answer RTMP requests! // I can answer RTMP requests!
req, err := rtmp.UnmarshalRequestPacket(pkt.Data) req, err := rtmp.UnmarshalRequestPacket(pkt.Data)
if err != nil { if err != nil {
log.Printf("RTMP: Couldn't unmarshal Request packet: %v", err) return fmt.Errorf("unmarshal Request packet: %w", 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 { switch req.Function {
case rtmp.FunctionRequest: case rtmp.FunctionRequest:
// Respond with RTMP Response // Respond with RTMP Response
respPkt := &rtmp.ResponsePacket{ respPkt := &rtmp.ResponsePacket{
SenderAddr: myAddr.Proto, SenderAddr: port.MyAddr,
Extended: true, Extended: true,
RangeStart: m.Config.EtherTalk.NetStart, RangeStart: port.NetStart,
RangeEnd: m.Config.EtherTalk.NetEnd, RangeEnd: port.NetEnd,
} }
respPktRaw, err := respPkt.Marshal() respPktRaw, err := respPkt.Marshal()
if err != nil { if err != nil {
log.Printf("RTMP: Couldn't marshal RTMP Response packet: %v", err) return fmt.Errorf("marshal RTMP Response packet: %w", err)
continue
} }
ddpPkt := &ddp.ExtPacket{ ddpPkt := &ddp.ExtPacket{
ExtHeader: ddp.ExtHeader{ ExtHeader: ddp.ExtHeader{
@ -115,26 +59,25 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
DstNet: pkt.SrcNet, DstNet: pkt.SrcNet,
DstNode: pkt.SrcNode, DstNode: pkt.SrcNode,
DstSocket: 1, // the RTMP socket DstSocket: 1, // the RTMP socket
SrcNet: myAddr.Proto.Network, SrcNet: port.MyAddr.Network,
SrcNode: myAddr.Proto.Node, SrcNode: port.MyAddr.Node,
SrcSocket: 1, // the RTMP socket SrcSocket: 1, // the RTMP socket
Proto: ddp.ProtoRTMPResp, Proto: ddp.ProtoRTMPResp,
}, },
Data: respPktRaw, Data: respPktRaw,
} }
if err := m.send(myAddr.Hardware, theirHWAddr, ddpPkt); err != nil { if err := port.Router.Output(ctx, ddpPkt); err != nil {
log.Printf("RTMP: Couldn't send Data broadcast: %v", err) return fmt.Errorf("send Response: %w", err)
} }
case rtmp.FunctionRDRSplitHorizon, rtmp.FunctionRDRComplete: case rtmp.FunctionRDRSplitHorizon, rtmp.FunctionRDRComplete:
// Like the Data broadcast, but solicited by a request (RDR). // Like the Data broadcast, but solicited by a request (RDR).
// TODO: handle split-horizon processing splitHorizon := req.Function == rtmp.FunctionRDRSplitHorizon
for _, dataPkt := range m.dataPackets(myAddr.Proto) { for _, dataPkt := range port.rtmpDataPackets(splitHorizon) {
dataPktRaw, err := dataPkt.Marshal() dataPktRaw, err := dataPkt.Marshal()
if err != nil { if err != nil {
log.Printf("RTMP: Couldn't marshal Data packet: %v", err) return fmt.Errorf("marshal RTMP Data packet: %w", err)
break
} }
ddpPkt := &ddp.ExtPacket{ ddpPkt := &ddp.ExtPacket{
@ -144,23 +87,22 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
DstNet: pkt.SrcNet, DstNet: pkt.SrcNet,
DstNode: pkt.SrcNode, DstNode: pkt.SrcNode,
DstSocket: 1, // the RTMP socket DstSocket: 1, // the RTMP socket
SrcNet: myAddr.Proto.Network, SrcNet: port.MyAddr.Network,
SrcNode: myAddr.Proto.Node, SrcNode: port.MyAddr.Node,
SrcSocket: 1, // the RTMP socket SrcSocket: 1, // the RTMP socket
Proto: ddp.ProtoRTMPResp, Proto: ddp.ProtoRTMPResp,
}, },
Data: dataPktRaw, Data: dataPktRaw,
} }
if err := m.send(myAddr.Hardware, theirHWAddr, ddpPkt); err != nil { if err := port.Router.Output(ctx, ddpPkt); err != nil {
log.Printf("RTMP: Couldn't send Data response: %v", err) return fmt.Errorf("send Data: %w", err)
break
} }
} }
case rtmp.FunctionLoopProbe: case rtmp.FunctionLoopProbe:
log.Print("RTMP: TODO: handle Loop Probes") log.Print("RTMP: TODO: handle Loop Probes")
return nil
} }
case ddp.ProtoRTMPResp: case ddp.ProtoRTMPResp:
@ -172,14 +114,12 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
break break
} }
peer := &EtherTalkPeer{ peer := &EtherTalkPeer{
PcapHandle: m.PcapHandle, Port: port,
MyHWAddr: m.AARP.myAddr.Hardware,
AARP: m.AARP,
PeerAddr: dataPkt.RouterAddr, PeerAddr: dataPkt.RouterAddr,
} }
for _, rt := range dataPkt.NetworkTuples { for _, rt := range dataPkt.NetworkTuples {
if err := m.RoutingTable.UpsertEthRoute(peer, rt.Extended, rt.RangeStart, rt.RangeEnd, rt.Distance+1); err != nil { 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) log.Printf("RTMP: Couldn't upsert EtherTalk route: %v", err)
} }
} }
@ -188,26 +128,51 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
log.Printf("RTMP: invalid DDP type %d on socket 1", pkt.Proto) log.Printf("RTMP: invalid DDP type %d on socket 1", pkt.Proto)
} }
return nil
}
// 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
<-port.AARPMachine.Assigned()
setStatus("Initial RTMP Data broadcast")
// Initial broadcast
if err := port.broadcastRTMPData(); err != nil {
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
}
setStatus("Starting broadcast loop")
bcastTicker := time.NewTicker(10 * time.Second)
defer bcastTicker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-bcastTicker.C:
setStatus("Broadcasting RTMP Data")
if err := port.broadcastRTMPData(); err != nil {
st := fmt.Sprintf("Couldn't broadcast Data: %v", err)
setStatus(st)
log.Print(st)
}
} }
} }
} }
func (m *RTMPMachine) send(src, dst ethernet.Addr, ddpPkt *ddp.ExtPacket) error { func (port *EtherTalkPort) broadcastRTMPData() error {
ethFrame, err := ethertalk.AppleTalk(src, *ddpPkt) for _, dataPkt := range port.rtmpDataPackets(true) {
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) {
dataPktRaw, err := dataPkt.Marshal() dataPktRaw, err := dataPkt.Marshal()
if err != nil { if err != nil {
return fmt.Errorf("marshal Data packet: %v", err) return fmt.Errorf("marshal Data packet: %v", err)
@ -217,29 +182,40 @@ func (m *RTMPMachine) broadcastData(myAddr aarp.AddrPair) error {
ExtHeader: ddp.ExtHeader{ ExtHeader: ddp.ExtHeader{
Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize, Size: uint16(len(dataPktRaw)) + atalk.DDPExtHeaderSize,
Cksum: 0, Cksum: 0,
DstNet: 0, // this network DstNet: 0x0000, // this network
DstNode: 0xff, // broadcast packet DstNode: 0xff, // broadcast packet
DstSocket: 1, // the RTMP socket DstSocket: 1, // the RTMP socket
SrcNet: myAddr.Proto.Network, SrcNet: port.MyAddr.Network,
SrcNode: myAddr.Proto.Node, SrcNode: port.MyAddr.Node,
SrcSocket: 1, // the RTMP socket SrcSocket: 1, // the RTMP socket
Proto: ddp.ProtoRTMPResp, Proto: ddp.ProtoRTMPResp,
}, },
Data: dataPktRaw, Data: dataPktRaw,
} }
if err := m.send(myAddr.Hardware, ethertalk.AppleTalkBroadcast, ddpPkt); err != nil { if err := port.Broadcast(ddpPkt); err != nil {
return err return err
} }
} }
return nil 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. // Build up a slice of routing tuples.
routes := m.RoutingTable.ValidRoutes() routes := port.Router.RouteTable.ValidRoutes()
tuples := make([]rtmp.NetworkTuple, 0, len(routes)) tuples := make([]rtmp.NetworkTuple, 0, len(routes))
for _, rt := range 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{ tuples = append(tuples, rtmp.NetworkTuple{
Extended: rt.Extended, Extended: rt.Extended,
RangeStart: rt.NetStart, RangeStart: rt.NetStart,
@ -253,8 +229,8 @@ func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket {
// TODO: support non-extended local networks (LocalTalk) // TODO: support non-extended local networks (LocalTalk)
first := rtmp.NetworkTuple{ first := rtmp.NetworkTuple{
Extended: true, Extended: true,
RangeStart: m.Config.EtherTalk.NetStart, RangeStart: port.NetStart,
RangeEnd: m.Config.EtherTalk.NetEnd, RangeEnd: port.NetEnd,
Distance: 0, Distance: 0,
} }
@ -274,7 +250,7 @@ func (m *RTMPMachine) dataPackets(myAddr ddp.Addr) []*rtmp.DataPacket {
rem = rem[len(chunk)-1:] rem = rem[len(chunk)-1:]
packets = append(packets, &rtmp.DataPacket{ packets = append(packets, &rtmp.DataPacket{
RouterAddr: myAddr, RouterAddr: port.MyAddr,
Extended: true, Extended: true,
NetworkTuples: chunk, NetworkTuples: chunk,
}) })

View file

@ -20,103 +20,29 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"slices"
"gitea.drjosh.dev/josh/jrouter/atalk" "gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/atp" "gitea.drjosh.dev/josh/jrouter/atalk/atp"
"gitea.drjosh.dev/josh/jrouter/atalk/zip" "gitea.drjosh.dev/josh/jrouter/atalk/zip"
"github.com/sfiera/multitalk/pkg/ddp" "github.com/sfiera/multitalk/pkg/ddp"
"github.com/sfiera/multitalk/pkg/ethernet" "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 { switch ddpkt.Proto {
case ddp.ProtoATP: case ddp.ProtoATP:
atpkt, err := atp.UnmarshalPacket(ddpkt.Data) return port.handleZIPATP(ctx, ddpkt)
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)
}
case ddp.ProtoZIP: 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) zipkt, err := zip.UnmarshalPacket(ddpkt.Data)
if err != nil { if err != nil {
return err return err
@ -124,8 +50,19 @@ func (rtr *Router) HandleZIP(ctx context.Context, srcHWAddr ethernet.Addr, ddpkt
switch zipkt := zipkt.(type) { switch zipkt := zipkt.(type) {
case *zip.QueryPacket: 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) log.Printf("ZIP: Got Query for networks %v", zipkt.Networks)
networks := rtr.ZoneTable.Query(zipkt.Networks) networks := port.Router.ZoneTable.Query(zipkt.Networks)
sendReply := func(resp *zip.ReplyPacket) error { sendReply := func(resp *zip.ReplyPacket) error {
respRaw, err := resp.Marshal() respRaw, err := resp.Marshal()
@ -139,14 +76,15 @@ func (rtr *Router) HandleZIP(ctx context.Context, srcHWAddr ethernet.Addr, ddpkt
DstNet: ddpkt.SrcNet, DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode, DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket, DstSocket: ddpkt.SrcSocket,
SrcNet: rtr.MyDDPAddr.Network, SrcNet: port.MyAddr.Network,
SrcNode: rtr.MyDDPAddr.Node, SrcNode: port.MyAddr.Node,
SrcSocket: 6, SrcSocket: 6,
Proto: ddp.ProtoZIP, Proto: ddp.ProtoZIP,
}, },
Data: respRaw, Data: respRaw,
} }
return rtr.sendEtherTalkDDP(srcHWAddr, outDDP) // Note: AARP can block
return port.Send(ctx, outDDP)
} }
// Inside AppleTalk SE, pp 8-11: // Inside AppleTalk SE, pp 8-11:
@ -216,20 +154,36 @@ func (rtr *Router) HandleZIP(ctx context.Context, srcHWAddr ethernet.Addr, ddpkt
} }
} }
return nil return nil
}
case *zip.GetNetInfoPacket: func (port *EtherTalkPort) handleZIPGetNetInfo(ctx context.Context, ddpkt *ddp.ExtPacket, zipkt *zip.GetNetInfoPacket) error {
log.Printf("ZIP: Got GetNetInfo for zone %q", zipkt.ZoneName) log.Printf("ZIP: Got GetNetInfo for zone %q", zipkt.ZoneName)
// Only running a network with one zone for now. // 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{ resp := &zip.GetNetInfoReplyPacket{
ZoneInvalid: zipkt.ZoneName != rtr.Config.EtherTalk.ZoneName, ZoneInvalid: !zoneValid,
UseBroadcast: false, UseBroadcast: false,
OnlyOneZone: true, OnlyOneZone: len(port.AvailableZones) == 1,
NetStart: rtr.Config.EtherTalk.NetStart, NetStart: port.NetStart,
NetEnd: rtr.Config.EtherTalk.NetEnd, NetEnd: port.NetEnd,
ZoneName: zipkt.ZoneName, // has to match request ZoneName: zipkt.ZoneName, // has to match request
MulticastAddr: atalk.MulticastAddr(rtr.Config.EtherTalk.ZoneName), MulticastAddr: mcastAddr,
DefaultZoneName: rtr.Config.EtherTalk.ZoneName, }
// 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) log.Printf("ZIP: Replying with GetNetInfo-Reply: %+v", resp)
@ -253,33 +207,113 @@ func (rtr *Router) HandleZIP(ctx context.Context, srcHWAddr ethernet.Addr, ddpkt
DstNet: ddpkt.SrcNet, DstNet: ddpkt.SrcNet,
DstNode: ddpkt.SrcNode, DstNode: ddpkt.SrcNode,
DstSocket: ddpkt.SrcSocket, DstSocket: ddpkt.SrcSocket,
SrcNet: rtr.MyDDPAddr.Network, SrcNet: port.MyAddr.Network,
SrcNode: rtr.MyDDPAddr.Node, SrcNode: port.MyAddr.Node,
SrcSocket: 6, SrcSocket: 6,
Proto: ddp.ProtoZIP, Proto: ddp.ProtoZIP,
}, },
Data: respRaw, Data: respRaw,
} }
// If it arrived as a broadcast, send the reply as a broadcast.
if ddpkt.DstNet == 0x0000 { if ddpkt.DstNet == 0x0000 {
outDDP.DstNet = 0x0000 outDDP.DstNet = 0x0000
} }
if ddpkt.DstNode == 0xFF { if ddpkt.DstNode == 0xFF {
outDDP.DstNode = 0xFF outDDP.DstNode = 0xFF
} }
// Note: AARP can block
// If it's a broadcast packet, broadcast it. Otherwise don't return port.Send(ctx, outDDP)
dstEth := ethertalk.AppleTalkBroadcast
if outDDP.DstNode != 0xFF {
dstEth = srcHWAddr
} }
return rtr.sendEtherTalkDDP(dstEth, 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: default:
return fmt.Errorf("TODO: handle type %T", zipkt) return fmt.Errorf("unsupported ATP packet type %T for ZIP", atpkt)
}
} }
default: func (port *EtherTalkPort) handleZIPTReq(ctx context.Context, ddpkt *ddp.ExtPacket, atpkt *atp.TReq) error {
return fmt.Errorf("invalid DDP type %d on socket 6", ddpkt.Proto) 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)
}

View file

@ -31,7 +31,7 @@ import (
type Zone struct { type Zone struct {
Network ddp.Network Network ddp.Network
Name string Name string
Local bool LocalPort *EtherTalkPort // nil if remote (local to another router)
LastSeen time.Time LastSeen time.Time
} }
@ -68,20 +68,20 @@ func (zt *ZoneTable) Dump() []Zone {
return zs 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() zt.mu.Lock()
defer zt.mu.Unlock() defer zt.mu.Unlock()
key := zoneKey{network, name} key := zoneKey{network, name}
z := zt.zones[key] z := zt.zones[key]
if z != nil { if z != nil {
z.Local = local z.LocalPort = localPort
z.LastSeen = time.Now() z.LastSeen = time.Now()
return return
} }
zt.zones[key] = &Zone{ zt.zones[key] = &Zone{
Network: network, Network: network,
Name: name, Name: name,
Local: local, LocalPort: localPort,
LastSeen: time.Now(), LastSeen: time.Now(),
} }
} }
@ -116,29 +116,29 @@ func (zt *ZoneTable) LookupName(name string) []*Zone {
return zs return zs
} }
func (zt *ZoneTable) LocalNames() []string { // func (zt *ZoneTable) LocalNames() []string {
zt.mu.Lock() // zt.mu.Lock()
seen := make(map[string]struct{}) // seen := make(map[string]struct{})
zs := make([]string, 0, len(zt.zones)) // zs := make([]string, 0, len(zt.zones))
for _, z := range zt.zones { // for _, z := range zt.zones {
// if time.Since(z.LastSeen) > maxZoneAge { // // if time.Since(z.LastSeen) > maxZoneAge {
// // continue
// // }
// if z.Local != nil {
// continue // continue
// } // }
if !z.Local { // if _, s := seen[z.Name]; s {
continue // continue
} // }
if _, s := seen[z.Name]; s { // seen[z.Name] = struct{}{}
continue // zs = append(zs, z.Name)
}
seen[z.Name] = struct{}{}
zs = append(zs, z.Name)
} // }
zt.mu.Unlock() // zt.mu.Unlock()
sort.Strings(zs) // sort.Strings(zs)
return zs // return zs
} // }
func (zt *ZoneTable) AllNames() []string { func (zt *ZoneTable) AllNames() []string {
zt.mu.Lock() zt.mu.Lock()