diff --git a/main.go b/main.go
index 2f5240d..f156af1 100644
--- a/main.go
+++ b/main.go
@@ -80,27 +80,6 @@ const routingTableTemplate = `
`
-const zoneTableTemplate = `
-
-
- Network |
- Name |
- Local Port |
- Last seen |
-
-
-{{range $zone := . }}
-
- {{$zone.Network}} |
- {{$zone.Name}} |
- {{with $zone.LocalPort}}{{.Device}}{{else}}-{{end}} |
- {{$zone.LastSeenAgo}} |
-
-{{end}}
-
-
-`
-
const peerTableTemplate = `
@@ -353,7 +332,7 @@ func main() {
NetStart: cfg.EtherTalk.NetStart,
NetEnd: cfg.EtherTalk.NetEnd,
DefaultZoneName: cfg.EtherTalk.ZoneName,
- AvailableZones: []string{cfg.EtherTalk.ZoneName},
+ AvailableZones: router.SetFromSlice([]string{cfg.EtherTalk.ZoneName}),
PcapHandle: pcapHandle,
AARPMachine: aarpMachine,
Router: rooter,
diff --git a/router/misc.go b/router/misc.go
new file mode 100644
index 0000000..e84f00e
--- /dev/null
+++ b/router/misc.go
@@ -0,0 +1,52 @@
+/*
+ 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
+
+// StringSet is a set of strings.
+// Yep, yet another string set implementation. Took me 2 minutes to write *shrug*
+type StringSet map[string]struct{}
+
+func (set StringSet) ToSlice() []string {
+ ss := make([]string, 0, len(set))
+ for s := range set {
+ ss = append(ss, s)
+ }
+ return ss
+}
+
+func (set StringSet) Contains(s string) bool {
+ _, c := set[s]
+ return c
+}
+
+func (set StringSet) Insert(ss ...string) {
+ for _, s := range ss {
+ set[s] = struct{}{}
+ }
+}
+
+func (set StringSet) Add(t StringSet) {
+ for s := range t {
+ set[s] = struct{}{}
+ }
+}
+
+func SetFromSlice(ss []string) StringSet {
+ set := make(StringSet, len(ss))
+ set.Insert(ss...)
+ return set
+}
diff --git a/router/nbp.go b/router/nbp.go
index 575b9f1..03514a6 100644
--- a/router/nbp.go
+++ b/router/nbp.go
@@ -20,7 +20,6 @@ import (
"context"
"fmt"
"log"
- "slices"
"gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/nbp"
@@ -170,7 +169,7 @@ func (rtr *Router) handleNBPFwdReq(ctx context.Context, ddpkt *ddp.ExtPacket, nb
tuple := &nbpkt.Tuples[0]
for _, outPort := range rtr.Ports {
- if !slices.Contains(outPort.AvailableZones, tuple.Zone) {
+ if !outPort.AvailableZones.Contains(tuple.Zone) {
continue
}
log.Printf("NBP: Converting FwdReq to LkUp (%v)", tuple)
diff --git a/router/peer_aurp.go b/router/peer_aurp.go
index 8e4bde9..e8b151c 100644
--- a/router/peer_aurp.go
+++ b/router/peer_aurp.go
@@ -618,7 +618,7 @@ func (p *AURPPeer) Handle(ctx context.Context) error {
case *aurp.ZIRspPacket:
log.Printf("AURP Peer: Learned about these zones: %v", pkt.Zones)
for _, zt := range pkt.Zones {
- p.RouteTable.AddZoneToNetwork(zt.Network, zt.Name)
+ p.RouteTable.AddZonesToNetwork(zt.Network, zt.Name)
}
case *aurp.GDZLReqPacket:
diff --git a/router/port.go b/router/port.go
index eb8aad6..760e013 100644
--- a/router/port.go
+++ b/router/port.go
@@ -37,7 +37,7 @@ type EtherTalkPort struct {
NetEnd ddp.Network
MyAddr ddp.Addr
DefaultZoneName string
- AvailableZones []string
+ AvailableZones StringSet
PcapHandle *pcap.Handle
AARPMachine *AARPMachine
Router *Router
diff --git a/router/route.go b/router/route.go
index f804383..bf16dfe 100644
--- a/router/route.go
+++ b/router/route.go
@@ -36,7 +36,7 @@ type Route struct {
// ZoneNames may be empty between learning the existence of a route and
// receiving zone information.
- ZoneNames []string
+ ZoneNames StringSet
// Exactly one of the following should be set
AURPPeer *AURPPeer // Next hop is this peer router (over AURP)
@@ -51,8 +51,11 @@ func (r Route) LastSeenAgo() string {
return fmt.Sprintf("%v ago", time.Since(r.LastSeen).Truncate(time.Millisecond))
}
+// Valid reports whether the route is valid.
+// A valid route has one or more zone names, and if it is learned from a peer
+// router over EtherTalk is not too old.
func (r *Route) Valid() bool {
- return r.EtherTalkPeer == nil || time.Since(r.LastSeen) <= maxRouteAge
+ return len(r.ZoneNames) > 0 && (r.EtherTalkPeer == nil || time.Since(r.LastSeen) <= maxRouteAge)
}
type RouteTable struct {
@@ -150,12 +153,12 @@ func (rt *RouteTable) UpdateAURPRouteDistance(peer *AURPPeer, network ddp.Networ
}
}
-func (rt *RouteTable) UpsertEtherTalkRoute(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) (*Route, error) {
if netStart > netEnd {
- return fmt.Errorf("invalid network range [%d, %d]", netStart, netEnd)
+ return nil, fmt.Errorf("invalid network range [%d, %d]", netStart, netEnd)
}
if netStart != netEnd && !extended {
- return fmt.Errorf("invalid network range [%d, %d] for nonextended network", netStart, netEnd)
+ return nil, fmt.Errorf("invalid network range [%d, %d] for nonextended network", netStart, netEnd)
}
rt.mu.Lock()
@@ -177,7 +180,7 @@ func (rt *RouteTable) UpsertEtherTalkRoute(peer *EtherTalkPeer, extended bool, n
}
r.Distance = metric
r.LastSeen = time.Now()
- return nil
+ return r, nil
}
// Insert.
@@ -190,7 +193,7 @@ func (rt *RouteTable) UpsertEtherTalkRoute(peer *EtherTalkPeer, extended bool, n
EtherTalkPeer: peer,
}
rt.routes[r] = struct{}{}
- return nil
+ return r, nil
}
func (rt *RouteTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart, netEnd ddp.Network, metric uint8) error {
@@ -216,6 +219,7 @@ func (rt *RouteTable) InsertAURPRoute(peer *AURPPeer, extended bool, netStart, n
return nil
}
+// ValidRoutes returns all valid routes.
func (rt *RouteTable) ValidRoutes() []*Route {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -228,6 +232,7 @@ func (rt *RouteTable) ValidRoutes() []*Route {
return valid
}
+// ValidNonAURPRoutes returns all valid routes that were not learned via AURP.
func (rt *RouteTable) ValidNonAURPRoutes() []*Route {
rt.mu.Lock()
defer rt.mu.Unlock()
diff --git a/router/rtmp.go b/router/rtmp.go
index bdfb6b5..d1a9752 100644
--- a/router/rtmp.go
+++ b/router/rtmp.go
@@ -24,6 +24,7 @@ import (
"gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/rtmp"
+ "gitea.drjosh.dev/josh/jrouter/atalk/zip"
"gitea.drjosh.dev/josh/jrouter/status"
"github.com/sfiera/multitalk/pkg/ddp"
@@ -110,18 +111,45 @@ func (port *EtherTalkPort) HandleRTMP(ctx context.Context, pkt *ddp.ExtPacket) e
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
+ return fmt.Errorf("unmarshal RTMP Data packet: %w", err)
}
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 {
+ var noZones []ddp.Network
+ for _, nt := range dataPkt.NetworkTuples {
+ route, err := port.Router.RouteTable.UpsertEtherTalkRoute(peer, nt.Extended, nt.RangeStart, nt.RangeEnd, nt.Distance+1)
+ if err != nil {
log.Printf("RTMP: Couldn't upsert EtherTalk route: %v", err)
}
+ if len(route.ZoneNames) == 0 {
+ noZones = append(noZones, route.NetStart)
+ }
+ }
+ if len(noZones) > 0 {
+ // Send a ZIP Query for all networks we don't have zone names for.
+ // TODO: split networks to fit in multiple packets as needed
+ qryPkt, err := (&zip.QueryPacket{Networks: noZones}).Marshal()
+ if err != nil {
+ return fmt.Errorf("marshal ZIP Query packet: %w", err)
+ }
+ outDDP := &ddp.ExtPacket{
+ ExtHeader: ddp.ExtHeader{
+ Size: uint16(len(qryPkt)) + atalk.DDPExtHeaderSize,
+ Cksum: 0,
+ SrcNet: port.MyAddr.Network,
+ SrcNode: port.MyAddr.Node,
+ SrcSocket: 6,
+ DstNet: pkt.SrcNet,
+ DstNode: pkt.SrcNode,
+ DstSocket: 6, // ZIP socket
+ Proto: ddp.ProtoZIP,
+ },
+ Data: qryPkt,
+ }
+ port.Send(ctx, outDDP)
}
default:
diff --git a/router/zip.go b/router/zip.go
index ed52f5b..c3b8875 100644
--- a/router/zip.go
+++ b/router/zip.go
@@ -20,7 +20,6 @@ import (
"context"
"fmt"
"log"
- "slices"
"gitea.drjosh.dev/josh/jrouter/atalk"
"gitea.drjosh.dev/josh/jrouter/atalk/atp"
@@ -52,6 +51,9 @@ func (port *EtherTalkPort) handleZIPZIP(ctx context.Context, ddpkt *ddp.ExtPacke
case *zip.QueryPacket:
return port.handleZIPQuery(ctx, ddpkt, zipkt)
+ case *zip.ReplyPacket:
+ return port.handleZIPReply(ctx, zipkt)
+
case *zip.GetNetInfoPacket:
return port.handleZIPGetNetInfo(ctx, ddpkt, zipkt)
@@ -156,11 +158,21 @@ func (port *EtherTalkPort) handleZIPQuery(ctx context.Context, ddpkt *ddp.ExtPac
return nil
}
+func (port *EtherTalkPort) handleZIPReply(ctx context.Context, zipkt *zip.ReplyPacket) error {
+ log.Printf("ZIP: Got Reply for networks %v", zipkt.Networks)
+
+ // Integrate new zone information into route table.
+ for n, zs := range zipkt.Networks {
+ port.Router.RouteTable.AddZonesToNetwork(n, zs...)
+ }
+ 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)
+ zoneValid := port.AvailableZones.Contains(zipkt.ZoneName)
// The multicast address we return depends on the validity of the zone
// name.
@@ -261,7 +273,7 @@ func (port *EtherTalkPort) handleZIPTReq(ctx context.Context, ddpkt *ddp.ExtPack
resp.Zones = port.Router.RouteTable.AllZoneNames()
case zip.FunctionGetLocalZones:
- resp.Zones = port.AvailableZones
+ resp.Zones = port.AvailableZones.ToSlice()
case zip.FunctionGetMyZone:
// Note: This shouldn't happen on extended networks (e.g. EtherTalk)
diff --git a/router/zones.go b/router/zones.go
index 5f8ca75..1dc3ead 100644
--- a/router/zones.go
+++ b/router/zones.go
@@ -22,7 +22,7 @@ import (
"github.com/sfiera/multitalk/pkg/ddp"
)
-func (rt *RouteTable) AddZoneToNetwork(n ddp.Network, z string) {
+func (rt *RouteTable) AddZonesToNetwork(n ddp.Network, zs ...string) {
rt.mu.Lock()
defer rt.mu.Unlock()
for r := range rt.routes {
@@ -32,10 +32,7 @@ func (rt *RouteTable) AddZoneToNetwork(n ddp.Network, z string) {
if !r.Valid() {
continue
}
- if slices.Contains(r.ZoneNames, z) {
- continue
- }
- r.ZoneNames = append(r.ZoneNames, z)
+ r.ZoneNames.Insert(zs...)
}
}
@@ -49,7 +46,9 @@ func (rt *RouteTable) ZonesForNetworks(ns []ddp.Network) map[ddp.Network][]strin
continue
}
if _, ok := slices.BinarySearch(ns, r.NetStart); ok {
- zs[r.NetStart] = append(zs[r.NetStart], r.ZoneNames...)
+ for z := range r.ZoneNames {
+ zs[r.NetStart] = append(zs[r.NetStart], z)
+ }
}
}
return zs
@@ -64,7 +63,7 @@ func (rt *RouteTable) RoutesForZone(zone string) []*Route {
if !r.Valid() {
continue
}
- if slices.Contains(r.ZoneNames, zone) {
+ if r.ZoneNames.Contains(zone) {
routes = append(routes, r)
}
}
@@ -77,19 +76,13 @@ func (rt *RouteTable) AllZoneNames() (zones []string) {
rt.mu.Lock()
defer rt.mu.Unlock()
- seen := make(map[string]struct{})
+ zs := make(StringSet)
for r := range rt.routes {
if !r.Valid() {
continue
}
- for _, z := range r.ZoneNames {
- if _, s := seen[z]; s {
- continue
- }
- seen[z] = struct{}{}
- zones = append(zones, z)
- }
+ zs.Add(r.ZoneNames)
}
- return zones
+ return zs.ToSlice()
}