Multi-port refactor #1
14 changed files with 961 additions and 869 deletions
|
@ -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)
|
||||||
|
|
279
main.go
279
main.go
|
@ -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,
|
RouteTable: routes,
|
||||||
MyHWAddr: myHWAddr,
|
ZoneTable: zones,
|
||||||
// MyDDPAddr: ...,
|
|
||||||
AARPMachine: aarpMachine,
|
|
||||||
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 ----------------------
|
// ---------------------- 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 {
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
if ddpkt.DstNode == 0 && localPort != nil { // Node 0 = any router for the network = me
|
||||||
// Is it addressed to me? Is it NBP?
|
// Is it NBP? FwdReq needs translating.
|
||||||
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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
245
router/nbp.go
245
router/nbp.go
|
@ -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,130 +42,180 @@ 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:
|
||||||
// There must be 1!
|
return port.handleNBPBrRq(ctx, ddpkt, nbpkt)
|
||||||
tuple := &nbpkt.Tuples[0]
|
|
||||||
|
|
||||||
zones := rtr.ZoneTable.LookupName(tuple.Zone)
|
default:
|
||||||
|
return fmt.Errorf("TODO: handle function %v", nbpkt.Function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, z := range zones {
|
func (port *EtherTalkPort) handleNBPBrRq(ctx context.Context, ddpkt *ddp.ExtPacket, nbpkt *nbp.Packet) error {
|
||||||
if z.Local {
|
// A BrRq was addressed to us. The sender (on a local network) is aware that
|
||||||
// If it's for the local zone, translate it to a LkUp and broadcast it back
|
// the network is extended and routed, and instead of broadcasting a LkUp
|
||||||
// out the EtherTalk port.
|
// itself, is asking us to do it.
|
||||||
// "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)
|
|
||||||
}
|
|
||||||
|
|
||||||
outDDP := ddp.ExtPacket{
|
// There must be 1!
|
||||||
ExtHeader: ddp.ExtHeader{
|
tuple := &nbpkt.Tuples[0]
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple)
|
// This logic would be required on a non-extended network:
|
||||||
if err := rtr.ZoneMulticastEtherTalkDDP(tuple.Zone, &outDDP); err != nil {
|
// if tuple.Zone == "" || tuple.Zone == "*" {
|
||||||
return err
|
// tuple.Zone = port.DefaultZoneName
|
||||||
}
|
// }
|
||||||
|
|
||||||
// But also...if we match the query, reply as though it was a LkUp
|
zones := port.Router.ZoneTable.LookupName(tuple.Zone)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
for _, z := range zones {
|
||||||
}
|
if outPort := z.LocalPort; outPort != nil {
|
||||||
|
// If it's for a local zone, translate it to a LkUp and broadcast
|
||||||
route := rtr.RouteTable.LookupRoute(z.Network)
|
// out the corresponding EtherTalk port.
|
||||||
if route == nil {
|
// "Note: On an internet, nodes on extended networks performing lookups in
|
||||||
return fmt.Errorf("no route for network %d", z.Network)
|
// 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
|
||||||
peer := route.AURPPeer
|
// lookups in their own zone will receive LkUp packets from themselves
|
||||||
if peer == nil {
|
// (actually sent by a router). The node's NBP process should expect to
|
||||||
return fmt.Errorf("nil peer for route for network %d", z.Network)
|
// receive these packets and must reply to them."
|
||||||
}
|
nbpkt.Function = nbp.FunctionLkUp
|
||||||
|
|
||||||
// Translate it into a FwdReq and route it to the
|
|
||||||
// routers with the appropriate zone(s).
|
|
||||||
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 LkUp: %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,
|
||||||
SrcNet: ddpkt.SrcNet,
|
SrcNet: port.MyAddr.Network,
|
||||||
SrcNode: ddpkt.SrcNode,
|
SrcNode: port.MyAddr.Node,
|
||||||
SrcSocket: ddpkt.SrcSocket,
|
SrcSocket: 2,
|
||||||
DstNet: z.Network,
|
DstNet: 0x0000, // Local network broadcast
|
||||||
DstNode: 0x00, // Any router for the dest network
|
DstNode: 0xFF, // Broadcast node address within the dest network
|
||||||
DstSocket: 2,
|
DstSocket: 2,
|
||||||
Proto: ddp.ProtoNBP,
|
Proto: ddp.ProtoNBP,
|
||||||
},
|
},
|
||||||
Data: nbpRaw,
|
Data: nbpRaw,
|
||||||
}
|
}
|
||||||
|
|
||||||
outDDPRaw, err := ddp.ExtMarshal(outDDP)
|
log.Printf("NBP: zone multicasting LkUp for tuple %v", tuple)
|
||||||
if err != nil {
|
if err := outPort.ZoneMulticast(tuple.Zone, &outDDP); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("NBP: Sending FwdReq to %v for tuple %v", peer.RemoteAddr, tuple)
|
// But also...if we match the query, reply as though it was a LkUp
|
||||||
|
// This uses the *input* port information.
|
||||||
if _, err := peer.Send(peer.Transport.NewAppleTalkPacket(outDDPRaw)); err != nil {
|
outDDP2, err := port.helloWorldThisIsMe(nbpkt.NBPID, tuple)
|
||||||
return fmt.Errorf("sending FwdReq on to peer: %w", err)
|
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:
|
// The zone table row is *not* for a local network.
|
||||||
return fmt.Errorf("TODO: handle function %v", nbpkt.Function)
|
// 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
|
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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
PeerAddr ddp.Addr
|
||||||
AARP *AARPMachine
|
|
||||||
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
193
router/port.go
Normal 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)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
ZoneTable *ZoneTable
|
||||||
MyDDPAddr ddp.Addr
|
Ports []*EtherTalkPort
|
||||||
AARPMachine *AARPMachine
|
|
||||||
RouteTable *RoutingTable
|
|
||||||
ZoneTable *ZoneTable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
dstEth = de
|
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 {
|
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
298
router/rtmp.go
298
router/rtmp.go
|
@ -26,44 +26,131 @@ 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
|
switch pkt.Proto {
|
||||||
Config *Config
|
case ddp.ProtoRTMPReq:
|
||||||
PcapHandle *pcap.Handle
|
// I can answer RTMP requests!
|
||||||
RoutingTable *RoutingTable
|
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.
|
// RunRTMP makes periodic RTMP Data broadcasts on this port.
|
||||||
func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket) error {
|
func (port *EtherTalkPort) RunRTMP(ctx context.Context) (err error) {
|
||||||
ctx, setStatus, done := status.AddSimpleItem(ctx, "RTMP")
|
ctx, setStatus, _ := status.AddSimpleItem(ctx, "RTMP")
|
||||||
defer done()
|
defer func() {
|
||||||
|
setStatus(fmt.Sprintf("Run loop stopped! Return: %v", err))
|
||||||
|
}()
|
||||||
|
|
||||||
setStatus("Awaiting DDP address assignment")
|
setStatus("Awaiting DDP address assignment")
|
||||||
|
|
||||||
// Await local address assignment before doing anything
|
// Await local address assignment before doing anything
|
||||||
<-m.AARP.Assigned()
|
<-port.AARPMachine.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")
|
setStatus("Initial RTMP Data broadcast")
|
||||||
|
|
||||||
// Initial 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)
|
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus("Packet loop")
|
setStatus("Starting broadcast loop")
|
||||||
|
|
||||||
bcastTicker := time.NewTicker(10 * time.Second)
|
bcastTicker := time.NewTicker(10 * time.Second)
|
||||||
defer bcastTicker.Stop()
|
defer bcastTicker.Stop()
|
||||||
|
@ -74,140 +161,18 @@ func (m *RTMPMachine) Run(ctx context.Context, incomingCh <-chan *ddp.ExtPacket)
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
|
|
||||||
case <-bcastTicker.C:
|
case <-bcastTicker.C:
|
||||||
if err := m.broadcastData(myAddr); err != nil {
|
setStatus("Broadcasting RTMP Data")
|
||||||
log.Printf("RTMP: Couldn't broadcast Data: %v", err)
|
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 {
|
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,
|
||||||
})
|
})
|
||||||
|
|
526
router/zip.go
526
router/zip.go
|
@ -20,266 +20,300 @@ 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:
|
||||||
zipkt, err := zip.UnmarshalPacket(ddpkt.Data)
|
return port.handleZIPZIP(ctx, ddpkt)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid DDP type %d on socket 6", ddpkt.Proto)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -29,10 +29,10 @@ import (
|
||||||
//const maxZoneAge = 10 * time.Minute // TODO: confirm
|
//const maxZoneAge = 10 * time.Minute // TODO: confirm
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z Zone) LastSeenAgo() string {
|
func (z Zone) LastSeenAgo() string {
|
||||||
|
@ -68,21 +68,21 @@ 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
|
// // continue
|
||||||
// }
|
// // }
|
||||||
if !z.Local {
|
// if z.Local != nil {
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
if _, s := seen[z.Name]; s {
|
// if _, s := seen[z.Name]; s {
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
seen[z.Name] = struct{}{}
|
// seen[z.Name] = struct{}{}
|
||||||
zs = append(zs, z.Name)
|
// 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()
|
||||||
|
|
Loading…
Reference in a new issue