From dd39ff5813bbb789b245e098df7e7a44e61c6b52 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Thu, 19 Mar 2026 14:51:16 -0700 Subject: [PATCH 01/38] fw: bier works now with merged code --- Makefile | 2 ++ fw/fw/bier_strategy.go | 3 +-- fw/fw/thread.go | 35 +++++++++++++++--------- repo/tlv/definitions.go | 2 +- repo/tlv/zz_generated.go | 57 +++++++++------------------------------- 5 files changed, 39 insertions(+), 60 deletions(-) diff --git a/Makefile b/Makefile index c0fa9e17..5ec4c696 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,8 @@ ndnd: clean CGO_ENABLED=0 go build -o ndnd \ -ldflags "-X '${STD_PACKAGE}/utils.NDNdVersion=${VERSION}'" \ cmd/ndnd/main.go + mkdir -p .bin/ + cp ndnd .bin/ examples: .bin/alo-latest diff --git a/fw/fw/bier_strategy.go b/fw/fw/bier_strategy.go index 6b9bc831..310dc8eb 100644 --- a/fw/fw/bier_strategy.go +++ b/fw/fw/bier_strategy.go @@ -72,8 +72,7 @@ func (s *BierStrategy) AfterReceiveInterest( packet *defn.Pkt, pitEntry table.PitEntry, inFace uint64, - nexthops []*table.FibNextHopEntry, - nextER []enc.Name, + nexthops []StrategyCandidateHop, ) { if len(packet.Bier) == 0 { // BierStrategy only handles BIER Interests. No broadcast fallback. diff --git a/fw/fw/thread.go b/fw/fw/thread.go index fccd2e8f..104daf5c 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -249,7 +249,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // Get strategy for name strategyName := table.FibStrategyTable.FindStrategyEnc(interest.Name()) strategy := t.strategies[strategyName.Hash()] - bestRouteStrategy := t.strategies[defn.BEST_ROUTE_STRATEGY.Hash()] // Add in-record and determine if already pending // this looks like custom interest again, but again can be changed without much issue? @@ -325,7 +324,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { routerName, routerNameSet := CfgRouterName() - nextLocal, nextNet, nextER, isMulticast := t.twoPhaseLookup(lookupName, packet.EgressRouter, routerName, routerNameSet, len(packet.Bier) > 0) + nextLocal, nextNet, isMulticast := t.twoPhaseLookup(lookupName, packet.EgressRouter, routerName, routerNameSet, len(packet.Bier) > 0) // If the first component is /localhop, we do not forward interests received // on non-local faces to non-local faces @@ -347,6 +346,10 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // Filter network FIB nexthops. allowedNetNexthops := make([]StrategyCandidateHop, 0, len(nextNet)) + + // List of unique egress routers in nexthops + allowedNetER := make([]enc.Name, 0) + allowedNetERSet := make(map[uint64]struct{}) for _, nexthop := range nextNet { // Exclude incoming face if nexthop.HopEntry.Nexthop == packet.IncomingFaceID { @@ -362,8 +365,17 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // Exclude faces that have an in-record for this interest // TODO: unclear where NFD dev guide specifies such behavior (if any) - if pitEntry.InRecords()[nexthop.HopEntry.Nexthop] == nil { - allowedNetNexthops = append(allowedNetNexthops, nexthop) + if pitEntry.InRecords()[nexthop.HopEntry.Nexthop] != nil { + continue + } + + allowedNetNexthops = append(allowedNetNexthops, nexthop) + if len(nexthop.EgressRouter) > 0 { + hash := nexthop.EgressRouter.Hash() + if _, ok := allowedNetERSet[hash]; !ok { + allowedNetERSet[hash] = struct{}{} + allowedNetER = append(allowedNetER, nexthop.EgressRouter.Clone()) + } } } @@ -412,7 +424,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } } // Unicast pipeline: delegate to strategy. - strategy.AfterReceiveInterest(packet, pitEntry, incomingFace.FaceID(), allowedNetNexthops, allowedNetER) + strategy.AfterReceiveInterest(packet, pitEntry, incomingFace.FaceID(), allowedNetNexthops) } else { // NACK? } @@ -435,8 +447,7 @@ func (t *Thread) processBierInterest(packet *defn.Pkt, pitEntry table.PitEntry, // performs the PET egress lookup. func (t *Thread) twoPhaseLookup(lookupName enc.Name, egressRouter enc.Name, routerName enc.Name, routerNameSet bool, hasBier bool) ( nextLocal []*table.PetNextHop, - nextNet []*table.FibNextHopEntry, - nextER []enc.Name, + nextNet []StrategyCandidateHop, isMulticast bool, ) { // If EgressRouter is set and matches this router, forward to local app faces via PET. @@ -448,7 +459,7 @@ func (t *Thread) twoPhaseLookup(lookupName enc.Name, egressRouter enc.Name, rout nextLocal = append(nextLocal, &petEntry.NextHops[i]) } } - return nextLocal, nextNet, nextER, false + return nextLocal, nextNet, false } else { for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(egressRouter) { nextNet = append(nextNet, StrategyCandidateHop{ @@ -456,14 +467,14 @@ func (t *Thread) twoPhaseLookup(lookupName enc.Name, egressRouter enc.Name, rout EgressRouter: egressRouter, }) } - return nextLocal, nextNet, nextER, false + return nextLocal, nextNet, false } } // No EgressRouter: try local PET first. petEntry, petFound := table.Pet.FindLongestPrefixEnc(lookupName) if !petFound { - return nextLocal, nextNet, nextER, false + return nextLocal, nextNet, false } for i := range petEntry.NextHops { nextLocal = append(nextLocal, &petEntry.NextHops[i]) @@ -472,7 +483,7 @@ func (t *Thread) twoPhaseLookup(lookupName enc.Name, egressRouter enc.Name, rout // When a BIER bit-string is already present the packet is on a transit router; // the BIFT + bit-string fully determines forwarding — no PET egress needed. if hasBier { - return nextLocal, nextNet, nextER, false + return nextLocal, nextNet, false } // Ingress (BFIR) path: map PET egress routers to network next-hops. @@ -485,7 +496,7 @@ func (t *Thread) twoPhaseLookup(lookupName enc.Name, egressRouter enc.Name, rout } } isMulticast = petEntry.Multicast - return nextLocal, nextNet, nextER, isMulticast + return nextLocal, nextNet, isMulticast } func (t *Thread) processOutgoingInterest( diff --git a/repo/tlv/definitions.go b/repo/tlv/definitions.go index 5ecc7e41..9285c98f 100644 --- a/repo/tlv/definitions.go +++ b/repo/tlv/definitions.go @@ -61,7 +61,7 @@ type BlobFetch struct { } type SecurityConfigObject struct { - //+field:sequence:[]byte:binary:[]byte + //+field:binary Schema []byte `tlv:"0x1A5"` //+field:sequence:[]byte:binary:[]byte Anchors [][]byte `tlv:"0x1BA"` diff --git a/repo/tlv/zz_generated.go b/repo/tlv/zz_generated.go index b94da29f..eed98344 100644 --- a/repo/tlv/zz_generated.go +++ b/repo/tlv/zz_generated.go @@ -1158,8 +1158,6 @@ func ParseBlobFetch(reader enc.WireView, ignoreCritical bool) (*BlobFetch, error type SecurityConfigObjectEncoder struct { Length uint - Schema_encoder struct { - } Anchors_subencoder []struct { } } @@ -1168,14 +1166,7 @@ type SecurityConfigObjectParsingContext struct { } func (encoder *SecurityConfigObjectEncoder) Init(value *SecurityConfigObject) { - { - encoder := &encoder.Schema_encoder - value := struct { - }{} - _ = encoder - _ = value - } { Anchors_l := len(value.Anchors) encoder.Anchors_subencoder = make([]struct { @@ -1198,22 +1189,10 @@ func (encoder *SecurityConfigObjectEncoder) Init(value *SecurityConfigObject) { } l := uint(0) - { - encoder := &encoder.Schema_encoder - value := struct { - Schema []byte - }{ - Schema: value.Schema, - } - { - if value.Schema != nil { - l += 3 - l += uint(enc.TLNum(len(value.Schema)).EncodingLength()) - l += uint(len(value.Schema)) - } - _ = encoder - _ = value - } + if value.Schema != nil { + l += 3 + l += uint(enc.TLNum(len(value.Schema)).EncodingLength()) + l += uint(len(value.Schema)) } if value.Anchors != nil { for seq_i, seq_v := range value.Anchors { @@ -1248,25 +1227,13 @@ func (encoder *SecurityConfigObjectEncoder) EncodeInto(value *SecurityConfigObje pos := uint(0) - { - encoder := &encoder.Schema_encoder - value := struct { - Schema []byte - }{ - Schema: value.Schema, - } - { - if value.Schema != nil { - buf[pos] = 253 - binary.BigEndian.PutUint16(buf[pos+1:], uint16(421)) - pos += 3 - pos += uint(enc.TLNum(len(value.Schema)).EncodeInto(buf[pos:])) - copy(buf[pos:], value.Schema) - pos += uint(len(value.Schema)) - } - _ = encoder - _ = value - } + if value.Schema != nil { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(421)) + pos += 3 + pos += uint(enc.TLNum(len(value.Schema)).EncodeInto(buf[pos:])) + copy(buf[pos:], value.Schema) + pos += uint(len(value.Schema)) } if value.Anchors != nil { for seq_i, seq_v := range value.Anchors { @@ -1384,7 +1351,7 @@ func (context *SecurityConfigObjectParsingContext) Parse(reader enc.WireView, ig value.Schema = nil } if !handled_Anchors && err == nil { - value.Anchors = nil + // sequence - skip } if err != nil { From 61f60d4fc602fb8b3085ddeecba22a351f96e18c Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 23 Mar 2026 01:51:57 -0700 Subject: [PATCH 02/38] fw: initial refactor into pipeline structure --- e2e/dv_util.py | 4 +- e2e/fw.py | 2 +- e2e/test_001.py | 4 +- e2e/test_007.py | 2 + fw/defn/name.go | 4 + fw/fw/thread.go | 480 +++++++++++++++++++++++++++++++++++------------- 6 files changed, 368 insertions(+), 128 deletions(-) diff --git a/e2e/dv_util.py b/e2e/dv_util.py index e70a15aa..5ce2518e 100644 --- a/e2e/dv_util.py +++ b/e2e/dv_util.py @@ -53,12 +53,14 @@ def is_converged(nodes: list[Node], network=DEFAULT_NETWORK, use_nfdc=False) -> # We don't support that. routes = node.cmd('nfdc route list') else: - routes = node.cmd('ndnd fw route-list') + routes = node.cmd('ndnd fw route-list 2>&1') for other in nodes: if other == node: continue if f'{network}/{other.name}' not in routes: info(f'Routing not converged on {node.name} for {other.name}\n') + preview = "\n".join(routes.splitlines()[:40]) + info(f'route-list({node.name}) first 40 lines:\n{preview}\n') converged = False break # break out of inner loop if not converged: diff --git a/e2e/fw.py b/e2e/fw.py index 5cd5f3b5..f6af09a0 100644 --- a/e2e/fw.py +++ b/e2e/fw.py @@ -5,7 +5,7 @@ from minindn.apps.application import Application class NDNd_FW(Application): - def __init__(self, node, config={}, logLevel='INFO', threads=2, network='/minindn', bier_index=-1): + def __init__(self, node, config={}, logLevel='TRACE', threads=2, network='/minindn', bier_index=-1): Application.__init__(self, node) if not shutil.which('ndnd'): diff --git a/e2e/test_001.py b/e2e/test_001.py index 569d5f74..319eb9d1 100644 --- a/e2e/test_001.py +++ b/e2e/test_001.py @@ -18,7 +18,7 @@ def scenario(ndn: Minindn, network='/minindn'): AppManager(ndn, ndn.net.hosts, NDNd_FW, network=network) dv_util.setup(ndn, network=network) - dv_util.converge(ndn.net.hosts, network=network) + dv_util.converge(ndn.net.hosts, network=network, deadline=20) info('Testing file transfer\n') test_file = '/tmp/test.bin' @@ -42,7 +42,7 @@ def scenario(ndn: Minindn, network='/minindn'): # New pipeline requires PET propagation before Interests can be forwarded. expected = {node: set(put_prefixes) for node in ndn.net.hosts} - dv_util.wait_prefix_pet_ready(expected, deadline=180) + dv_util.wait_prefix_pet_ready(expected, deadline=20) # Prefix traffic should remain PET-driven; app prefixes must not be injected into FIB. for node in ndn.net.hosts: diff --git a/e2e/test_007.py b/e2e/test_007.py index 76150793..140e0c37 100644 --- a/e2e/test_007.py +++ b/e2e/test_007.py @@ -81,6 +81,7 @@ def scenario(ndn: Minindn, network='/minindn'): producer_prefixes = {sync_prefix, f'{network}/svs/{producer.name}'} dv_util.wait_prefix_pet_ready({node: producer_prefixes for node in consumers}, deadline=60) + beg = time.time() # Step 5: Wait for publish → BIER sync interest propagation → unicast data fetch. # 20s delay + 30s SVS sync cycle + 20s fetch buffer = 70s total. info(f'Waiting for message propagation from {producer.name} (70s)...\n') @@ -90,6 +91,7 @@ def scenario(ndn: Minindn, network='/minindn'): for node in chat_nodes: node.cmd("pkill -f 'ndnd svs-chat' 2>/dev/null; true") time.sleep(1) # let log buffers flush + print(time.time() - beg, "seconds elapsed") failures = [] for consumer in consumers: diff --git a/fw/defn/name.go b/fw/defn/name.go index 4c4aeaca..a3ea09d6 100644 --- a/fw/defn/name.go +++ b/fw/defn/name.go @@ -15,5 +15,9 @@ var BEST_ROUTE_STRATEGY = STRATEGY_PREFIX. Append(enc.NewGenericComponent("best-route")). Append(enc.NewVersionComponent(1)) +var BROADCAST_STRATEGY = STRATEGY_PREFIX. + Append(enc.NewGenericComponent("broadcast")). + Append(enc.NewVersionComponent(1)) + // Default forwarding strategy name var DEFAULT_STRATEGY = BEST_ROUTE_STRATEGY diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 104daf5c..ac3637bb 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -22,6 +22,33 @@ import ( "github.com/named-data/ndnd/std/utils" ) +type forwardPipeline int + +const ( + fwUnicastTransit forwardPipeline = iota + fwUnicastIngress + fwUnicastEgress + fwMulticastIngress + fwMulticastEgress + fwMulticastTransit +) + +func (p forwardPipeline) isUnicast() bool { + return p == fwUnicastTransit || p == fwUnicastIngress || p == fwUnicastEgress +} + +func (p forwardPipeline) isMulticast() bool { + return !p.isUnicast() +} + +func (p forwardPipeline) isTransit() bool { + return p == fwUnicastTransit || p == fwMulticastTransit +} + +func (p forwardPipeline) isIngressEgress() bool { + return !p.isTransit() +} + // MaxFwThreads Maximum number of forwarding threads const MaxFwThreads = 32 @@ -246,7 +273,66 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { return } - // Get strategy for name + // Use forwarding hint if present + lookupName := interest.Name() + if fhName != nil { + lookupName = fhName + } + + routerName, routerNameSet := CfgRouterName() + + // TODO - use an actual table instead of these if statements + // If the first component is /localhop, we do not forward interests received + // on non-local faces to non-local faces + isLocalHop := packet.Name.At(0).Equal(enc.LOCALHOP) + // Detect DV advertisement fetches: /localhop///DV/ADV/... + isDvAdvLocalhop := isLocalHop && len(packet.Name) >= 5 && + packet.Name[3].Equal(enc.NewKeywordComponent("DV")) && + packet.Name[4].Equal(enc.NewKeywordComponent("ADV")) + // Detect DV SVS control traffic: //DV/PES/svs/... or /localhop//DV/PES/svs/... + isDvSvs := false + if isLocalHop { + isDvSvs = len(packet.Name) >= 5 && + packet.Name[2].Equal(enc.NewKeywordComponent("DV")) && + packet.Name[3].Equal(enc.NewKeywordComponent("PES")) && + packet.Name[4].Equal(enc.NewKeywordComponent("svs")) + } else { + isDvSvs = len(packet.Name) >= 4 && + packet.Name[1].Equal(enc.NewKeywordComponent("DV")) && + packet.Name[2].Equal(enc.NewKeywordComponent("PES")) && + packet.Name[3].Equal(enc.NewKeywordComponent("svs")) + } + + var pipeline forwardPipeline + var petEntry table.PetEntry + var petFound bool + petLookup := false + if len(packet.Bier) > 0 { + // TODO - distinguish fwMulticastEgress + pipeline = fwMulticastTransit + } else if len(packet.EgressRouter) > 0 { + if isDvSvs { + packet.EgressRouter = nil + petLookup = true + petEntry, petFound = table.Pet.FindLongestPrefixEnc(lookupName) + pipeline = fwMulticastIngress + } else if routerNameSet && packet.EgressRouter.Equal(routerName) { + pipeline = fwUnicastEgress + } else { + pipeline = fwUnicastTransit + } + } else { + petLookup = true + petEntry, petFound = table.Pet.FindLongestPrefixEnc(lookupName) + if isDvSvs || packet.Name.At(0).Equal(enc.LOCALHOP) || (petFound && petEntry.Multicast) { + pipeline = fwMulticastIngress + } else { + pipeline = fwUnicastIngress + } + } + + // Get strategy for name. This may not be the final strategy, + // we need to get a prelim strategy for CS though for AfterContentStoreHit strategyName := table.FibStrategyTable.FindStrategyEnc(interest.Name()) strategy := t.strategies[strategyName.Hash()] @@ -255,48 +341,58 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { inRecord, isAlreadyPending, prevNonce := pitEntry.InsertInRecord( interest, incomingFace.FaceID(), packet.PitToken) - if !isAlreadyPending { - core.Log.Trace(t, "Interest is not pending", "name", packet.Name) - - // Check CS for matching entry - if t.pitCS.IsCsServing() { - csEntry := t.pitCS.FindMatchingDataFromCS(interest) - if csEntry != nil { - // Update counters - t.nCsHits.Add(1) - - // Parse the cached data packet and replace in the pending one - // This is not the fastest way to do it, but simplifies everything - // significantly. We can optimize this later. - csData, csWire, err := csEntry.Copy() - if csData != nil && csWire != nil { - // Mark PIT entry as expired - table.UpdateExpirationTimer(pitEntry, time.Now()) - - // Create the pending packet structure - packet.EgressRouter = nil - packet.L3.Data = csData - packet.L3.Interest = nil - packet.Raw = enc.Wire{csWire} - packet.Name = csData.NameV - strategy.AfterContentStoreHit(packet, pitEntry, incomingFace.FaceID()) - return - } else if err != nil { - core.Log.Error(t, "Error copying CS entry", "err", err) - } else { - core.Log.Error(t, "Error copying CS entry", "err", "csData is nil") - } - } else { - // Update counters - t.nCsMisses.Add(1) - } - } - } else { + if isAlreadyPending { core.Log.Trace(t, "Interest is already pending", "name", packet.Name) // Add the previous nonce to the dead nonce list to prevent further looping // TODO: review this design, not specified in NFD dev guide t.deadNonceList.Insert(interest.Name(), prevNonce) + } else { + core.Log.Trace(t, "Interest is not pending", "name", packet.Name) + } + + // Check CS for matching entry + if pipeline.isUnicast() && !isAlreadyPending && t.pitCS.IsCsServing() { + csEntry := t.pitCS.FindMatchingDataFromCS(interest) + if csEntry != nil { + // Update counters + t.nCsHits.Add(1) + + // Parse the cached data packet and replace in the pending one + // This is not the fastest way to do it, but simplifies everything + // significantly. We can optimize this later. + csData, csWire, err := csEntry.Copy() + if csData != nil && csWire != nil { + // Mark PIT entry as expired + table.UpdateExpirationTimer(pitEntry, time.Now()) + + // Create the pending packet structure + packet.EgressRouter = nil + packet.L3.Data = csData + packet.L3.Interest = nil + packet.Raw = enc.Wire{csWire} + packet.Name = csData.NameV + strategy.AfterContentStoreHit(packet, pitEntry, incomingFace.FaceID()) + // TODO - below is equivalent to above AfterContentStoreHit call. replace above with below to not require having a strategy locked in at this stage. + // core.Log.Trace(t, "AfterContentStoreHit", "name", packet.Name, "faceid", incomingFace.FaceID()) + + // nexthop := incomingFace.FaceID() + // var pitToken []byte + // if inRecord, ok := pitEntry.InRecords()[nexthop]; ok { + // pitToken = inRecord.PitToken + // pitEntry.RemoveInRecord(nexthop) + // } + // t.processOutgoingData(packet, nexthop, pitToken, 0) + return + } else if err != nil { + core.Log.Error(t, "Error copying CS entry", "err", err) + } else { + core.Log.Error(t, "Error copying CS entry", "err", "csData is nil") + } + } else { + // Update counters + t.nCsMisses.Add(1) + } } // Update PIT entry expiration timer @@ -316,117 +412,253 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { return } - // Use forwarding hint if present - lookupName := interest.Name() - if fhName != nil { - lookupName = fhName - } - - routerName, routerNameSet := CfgRouterName() - - nextLocal, nextNet, isMulticast := t.twoPhaseLookup(lookupName, packet.EgressRouter, routerName, routerNameSet, len(packet.Bier) > 0) + var petLocalHops []*table.PetNextHop - // If the first component is /localhop, we do not forward interests received - // on non-local faces to non-local faces - localFacesOnly := incomingFace.Scope() != defn.Local && packet.Name.At(0).Equal(enc.LOCALHOP) + if pipeline.isIngressEgress() { + // perform cached pet lookup + if !petLookup { + petEntry, petFound = table.Pet.FindLongestPrefixEnc(lookupName) + petLookup = true + } - // Filter local PET nexthops. - allowedLocalNexthops := make([]*table.PetNextHop, 0, len(nextLocal)) - for _, nexthop := range nextLocal { - // Exclude incoming face - if nexthop.FaceID == packet.IncomingFaceID { - continue + // drop if no possible egress / local faces found + if !petFound && !strategyName.Equal(defn.BROADCAST_STRATEGY) && !isDvAdvLocalhop && !isDvSvs { + core.Log.Trace(t, "Dropping Interest: no PET entry", + "name", packet.Name, + "lookup", lookupName, + "pipeline", pipeline, + ) + return } - // Exclude faces that have an in-record for this interest - // TODO: unclear where NFD dev guide specifies such behavior (if any) - if pitEntry.InRecords()[nexthop.FaceID] == nil { - allowedLocalNexthops = append(allowedLocalNexthops, nexthop) + + if petFound { + petLocalHops = make([]*table.PetNextHop, 0, len(petEntry.NextHops)) + for i := range petEntry.NextHops { + nexthop := &petEntry.NextHops[i] + // Exclude incoming face + if nexthop.FaceID == packet.IncomingFaceID { + continue + } + // Exclude faces that have an in-record for this interest + // TODO: unclear where NFD dev guide specifies such behavior (if any) + if pitEntry.InRecords()[nexthop.FaceID] != nil { + continue + } + petLocalHops = append(petLocalHops, nexthop) + } } } - // Filter network FIB nexthops. - allowedNetNexthops := make([]StrategyCandidateHop, 0, len(nextNet)) + // nextLocal, nextNet, isMulticast := t.twoPhaseLookup(lookupName, packet.EgressRouter, routerName, routerNameSet, len(packet.Bier) > 0) - // List of unique egress routers in nexthops - allowedNetER := make([]enc.Name, 0) - allowedNetERSet := make(map[uint64]struct{}) - for _, nexthop := range nextNet { - // Exclude incoming face - if nexthop.HopEntry.Nexthop == packet.IncomingFaceID { - continue + localFacesOnly := incomingFace.Scope() != defn.Local && isLocalHop + + // Unicast Delivery making use of FIB -> FIBStrategy -> unicast delivery + if pipeline.isUnicast() { + // Deliver to local faces if there exist such legal local faces + if len(petLocalHops) > 0 { + core.Log.Trace(t, "Local Egress captures the Interest", "name", packet.Name) + packet.EgressRouter = nil + // TODO: micro-strategy dispatch per PIT in-record + t.processOutgoingInterest(packet, pitEntry, petLocalHops[0].FaceID, incomingFace.FaceID()) + // For broadcast-style control traffic (e.g., localhop DV or SVS), + // continue to network forwarding even after local delivery. + if !isLocalHop && !strategyName.Equal(defn.BROADCAST_STRATEGY) { + return + } } - // Exclude non-local faces for localhop enforcement - if localFacesOnly { - if face := dispatch.GetFace(nexthop.HopEntry.Nexthop); face != nil && face.Scope() != defn.Local { + // FIB lookup to get the next network hops + nextNet := make([]StrategyCandidateHop, 0) + if len(packet.EgressRouter) > 0 { + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(packet.EgressRouter) { + nextNet = append(nextNet, StrategyCandidateHop{ + HopEntry: nextHop, + EgressRouter: packet.EgressRouter, + }) + } + } else if petFound { + for _, er := range petEntry.EgressRouters { + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + nextNet = append(nextNet, StrategyCandidateHop{ + HopEntry: nextHop, + EgressRouter: er, + }) + } + } + } else if strategyName.Equal(defn.BROADCAST_STRATEGY) || isDvAdvLocalhop { + er := enc.Name{enc.LOCALHOP, enc.NewGenericComponent("neighbors")} + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + nextNet = append(nextNet, StrategyCandidateHop{ + HopEntry: nextHop, + EgressRouter: er, + }) + } + } else if isDvSvs { + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(lookupName) { + nextNet = append(nextNet, StrategyCandidateHop{ + HopEntry: nextHop, + EgressRouter: nil, + }) + } + } else { + core.Log.Trace(t, "Unicast forwarding: no PET and no fallback UNREACHABLE", + "name", packet.Name, + "lookup", lookupName, + "pipeline", pipeline, + "strategy", strategyName, + "isDvAdvLocalhop", isDvAdvLocalhop, + "isDvSvs", isDvSvs, + ) + } + + // Filter network FIB nexthops. + allowedNetNexthops := make([]StrategyCandidateHop, 0, len(nextNet)) + + for _, nexthop := range nextNet { + // Exclude incoming face + if nexthop.HopEntry.Nexthop == packet.IncomingFaceID { + continue + } + + // Exclude non-local faces for localhop enforcement + if localFacesOnly { + if face := dispatch.GetFace(nexthop.HopEntry.Nexthop); face != nil && face.Scope() != defn.Local { + continue + } + } + + // Exclude faces that have an in-record for this interest + // TODO: unclear where NFD dev guide specifies such behavior (if any) + if pitEntry.InRecords()[nexthop.HopEntry.Nexthop] != nil { continue } + + allowedNetNexthops = append(allowedNetNexthops, nexthop) } - // Exclude faces that have an in-record for this interest - // TODO: unclear where NFD dev guide specifies such behavior (if any) - if pitEntry.InRecords()[nexthop.HopEntry.Nexthop] != nil { - continue + if len(allowedNetNexthops) > 0 { + // Forward using a unicast strategy from FibStrategyTable + // Below does a FibStrategyTable lookup, but we only have one strategy rn + // so we manually use bestRouteStrategy for testing + // strategyName := table.FibStrategyTable.FindStrategyEnc(interest.Name()) + // strategy := t.strategies[strategyName.Hash()] + bestRouteStrategy := t.strategies[defn.BEST_ROUTE_STRATEGY.Hash()] + strategy := bestRouteStrategy + strategy.AfterReceiveInterest(packet, pitEntry, incomingFace.FaceID(), allowedNetNexthops) + } else { + // NACK? + return + } + } + + // Multicast delivery making use of BIFT -> MulticastStrategyTable -> multicast delivery + // Currently just overrides /localhop to a broadcast override and manual BIER code + if pipeline.isMulticast() { + // Deliver to local faces if there exist such legal local faces + deliverToLocal := len(petLocalHops) > 0 + if deliverToLocal { + core.Log.Trace(t, "Local Egress captures the Interest", "name", packet.Name) + // In multicast, we deliver to all local faces unlike in unicast + for _, localHop := range petLocalHops { + packet.EgressRouter = nil + t.processOutgoingInterest(packet, pitEntry, localHop.FaceID, incomingFace.FaceID()) + } + if !IsBierEnabled() { + if !isDvSvs && !isLocalHop { + return + } + } } - allowedNetNexthops = append(allowedNetNexthops, nexthop) - if len(nexthop.EgressRouter) > 0 { - hash := nexthop.EgressRouter.Hash() - if _, ok := allowedNetERSet[hash]; !ok { - allowedNetERSet[hash] = struct{}{} - allowedNetER = append(allowedNetER, nexthop.EgressRouter.Clone()) + if isDvSvs && !isLocalHop { + er := enc.Name{enc.LOCALHOP, enc.NewGenericComponent("neighbors")} + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + // Exclude incoming face + if nextHop.Nexthop == packet.IncomingFaceID { + continue + } + // Exclude non-local faces for localhop enforcement + if localFacesOnly { + if face := dispatch.GetFace(nextHop.Nexthop); face != nil && face.Scope() != defn.Local { + continue + } + } + // For DV SVS control, do not suppress forwarding based on PIT in-records. + t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) } + return } - } - // If local choices exist, forward to local app face(s). - // For BIER interests, after local delivery clear the local bit and continue - // to network replication so the router can act as both BFER and BFR. - if len(allowedLocalNexthops) > 0 { - core.Log.Trace(t, "Local Egress captures the Interest", "name", packet.Name) - packet.EgressRouter = nil - // TODO: micro-strategy dispatch per PIT in-record - t.processOutgoingInterest(packet, pitEntry, allowedLocalNexthops[0].FaceID, incomingFace.FaceID()) - // If this is a BIER interest, clear our local bit and continue to network replication - if len(packet.Bier) > 0 && IsBierEnabled() { - bs := BierClone(packet.Bier) - BierClearBit(bs, CfgBierIndex()) - packet.Bier = bs - if BierIsZero(packet.Bier) { - return + if IsBierEnabled() && !isLocalHop { + core.Log.Trace(t, "Multicast BIER path", + "name", packet.Name, + "bier", len(packet.Bier), + "egressRouters", len(petEntry.EgressRouters), + ) + if len(packet.Bier) == 0 { + if pipeline != fwMulticastIngress { + core.Log.Trace(t, "Multicast BIER empty, non-ingress UNREACHABLE", + "name", packet.Name, + "pipeline", pipeline, + ) + return + } + core.Log.Trace(t, "BFIR: encoding BIER bit-string", "name", packet.Name, "egress-count", len(petEntry.EgressRouters)) + packet.Bier = Bift.BuildBierBitString(petEntry.EgressRouters) } - // Fall through to network BIER replication below - } else { + if deliverToLocal && len(packet.Bier) > 0 && IsBierEnabled() { + bs := BierClone(packet.Bier) + BierClearBit(bs, CfgBierIndex()) + packet.Bier = bs + if BierIsZero(bs) { + return + } + } + + t.processBierInterest(packet, pitEntry, incomingFace.FaceID()) return } - } - // Transit BFR: packet arrived with a BIER bit-string already set. - // Dispatch directly to the multicast pipeline — BIFT determines next hops, - // independent of the PET/FIB allowedNetNexthops list (which was intentionally - // left empty by twoPhaseLookup when hasBier=true). - if len(packet.Bier) > 0 { - t.processBierInterest(packet, pitEntry, incomingFace.FaceID()) - return - } + if isLocalHop && !petFound { + er := enc.Name{enc.LOCALHOP, enc.NewGenericComponent("neighbors")} + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + // Enforce localhop: only forward to local faces when incoming is non-local. + if localFacesOnly { + if face := dispatch.GetFace(nextHop.Nexthop); face != nil && face.Scope() != defn.Local { + continue + } + } + // Exclude incoming face + if nextHop.Nexthop == packet.IncomingFaceID { + continue + } + // Exclude faces that have an in-record for this interest + if pitEntry.InRecords()[nextHop.Nexthop] != nil { + continue + } + t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) + } + return + } - if len(allowedNetNexthops) > 0 { - // BFIR: when this router is the ingress and sees a Sync group prefix - // (isMulticast=true) with >1 egress routers and BIER enabled, encode a - // bit-string. After this point packet.Bier is set for all BIER traffic. - if IsBierEnabled() && len(allowedNetER) > 1 && isMulticast { - bierBs := Bift.BuildBierBitString(allowedNetER) - if len(bierBs) > 0 { - core.Log.Trace(t, "BFIR: encoding BIER bit-string", "name", packet.Name, "egress-count", len(allowedNetER)) - packet.Bier = bierBs - t.processBierInterest(packet, pitEntry, incomingFace.FaceID()) - return + if !petFound { + core.Log.Trace(t, "localhop, petEntry = nil, UNREACHABLE") + return + } + // TODO - why do we need to do a FIB lookup in this case? makes no sense? + // Can we just replace this with the actual adjacent faces? + for _, er := range petEntry.EgressRouters { + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) } } - // Unicast pipeline: delegate to strategy. - strategy.AfterReceiveInterest(packet, pitEntry, incomingFace.FaceID(), allowedNetNexthops) - } else { - // NACK? + return + + // is this needed here or will this be later? + // if BierIsZero(packet.Bier) { + // return + // } } } From bfd0dac75362c4c51833759a1c46352c0d91210c Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 23 Mar 2026 17:07:16 -0700 Subject: [PATCH 03/38] fw: use PET multicast flag on sync / advertisement rather than strategy --- dv/dv/router.go | 27 +++------------------------ e2e/test_001.py | 10 ---------- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/dv/dv/router.go b/dv/dv/router.go index 10663a8e..13d72386 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -296,12 +296,14 @@ func (dv *Router) register() (err error) { } // Allow outgoing local-prefix-sync Interests to use two-phase forwarding. // Incoming Interests still terminate locally on the same prefix. + // Add multicast flag to the PET announcement dv.nfdc.Exec(nfdc.NfdMgmtCmd{ Module: "pet", Cmd: "add-egress", Args: &mgmt.ControlArgs{ Name: dv.pfx.SyncPrefix(), Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, + Flags: optional.Some(uint64(1)), }, Retries: -1, }) @@ -312,30 +314,7 @@ func (dv *Router) register() (err error) { Args: &mgmt.ControlArgs{ Name: dv.config.AdvertisementSyncPrefix(), Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, - }, - Retries: -1, - }) - // Set strategy to broadcast for advertisement sync Interests, so - // /localhop/.../DV/ADS traffic fan-outs to all neighbor nexthops. - dv.nfdc.Exec(nfdc.NfdMgmtCmd{ - Module: "strategy-choice", - Cmd: "set", - Args: &mgmt.ControlArgs{ - Name: dv.config.AdvertisementSyncPrefix(), - Strategy: &mgmt.Strategy{ - Name: config.BroadcastStrategy, - }, - }, - Retries: -1, - }) - dv.nfdc.Exec(nfdc.NfdMgmtCmd{ - Module: "strategy-choice", - Cmd: "set", - Args: &mgmt.ControlArgs{ - Name: dv.pfx.SyncPrefix(), - Strategy: &mgmt.Strategy{ - Name: config.BroadcastStrategy, - }, + Flags: optional.Some(uint64(1)), }, Retries: -1, }) diff --git a/e2e/test_001.py b/e2e/test_001.py index 319eb9d1..2f1cbe30 100644 --- a/e2e/test_001.py +++ b/e2e/test_001.py @@ -63,16 +63,6 @@ def scenario(ndn: Minindn, network='/minindn'): strategy = node.cmd('ndnd fw strategy-list') if "multicast" in strategy: raise Exception(f'Multicast is to be retired, unexpectedly present in strategy on {node.name}') - expected_strategies = [ - # Localhop SVS sync interests should use broadcast (#174) - "prefix=/minindn/32=DV/32=PES/32=svs strategy=/localhost/nfd/strategy/broadcast/v=1", - - # Localhop advertisement sync interests should use broadcast strategy (#174) - "prefix=/localhop/minindn/32=DV/32=ADS strategy=/localhost/nfd/strategy/broadcast/v=1", - ] - for expected_strat in expected_strategies: - if expected_strat not in strategy: - raise Exception(f'Strategy {expected_strat!r} not in strategy on {node.name}') for node, put_node in cat_requests: cmd = f'ndnd cat "{network}/{put_node.name}/test" > recv.test.bin 2> cat.log' From cc00471a9888569d05b49843b5d7cfb8b7bd78ce Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 23 Mar 2026 22:59:20 -0700 Subject: [PATCH 04/38] fw: get rid of isDvSvs and isDvAdvLocalhop overrides --- e2e/test_005.py | 4 +- fw/fw/thread.go | 140 +++++++++++++++++++++++------------------------- 2 files changed, 70 insertions(+), 74 deletions(-) diff --git a/e2e/test_005.py b/e2e/test_005.py index 7e344f2e..de4b280b 100644 --- a/e2e/test_005.py +++ b/e2e/test_005.py @@ -41,7 +41,7 @@ def scenario(ndn: Minindn, network='/minindn'): AppManager(ndn, [host], NDNd_FW, network=network, bier_index=bier_map[host]) dv_util.setup(ndn, network=network) - dv_util.converge(ndn.net.hosts, network=network) + dv_util.converge(ndn.net.hosts, network=network, deadline=20) dv_util.populate_bift(hosts, bier_map, network=network) producer = sorted(hosts, key=lambda h: h.name)[0] @@ -57,7 +57,7 @@ def scenario(ndn: Minindn, network='/minindn'): # Wait for prefix to appear in PET on every node (confirms BIER egress table is ready) expected = {node: {prefix} for node in hosts} - dv_util.wait_prefix_pet_ready(expected, deadline=180) + dv_util.wait_prefix_pet_ready(expected, deadline=20) info('--- Fetching data via BIER multicast on all consumers ---\n') failures = [] diff --git a/fw/fw/thread.go b/fw/fw/thread.go index ac3637bb..6eac565e 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -285,24 +285,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // If the first component is /localhop, we do not forward interests received // on non-local faces to non-local faces isLocalHop := packet.Name.At(0).Equal(enc.LOCALHOP) - // Detect DV advertisement fetches: /localhop///DV/ADV/... - isDvAdvLocalhop := isLocalHop && len(packet.Name) >= 5 && - packet.Name[3].Equal(enc.NewKeywordComponent("DV")) && - packet.Name[4].Equal(enc.NewKeywordComponent("ADV")) - // Detect DV SVS control traffic: //DV/PES/svs/... or /localhop//DV/PES/svs/... - isDvSvs := false - if isLocalHop { - isDvSvs = len(packet.Name) >= 5 && - packet.Name[2].Equal(enc.NewKeywordComponent("DV")) && - packet.Name[3].Equal(enc.NewKeywordComponent("PES")) && - packet.Name[4].Equal(enc.NewKeywordComponent("svs")) - } else { - isDvSvs = len(packet.Name) >= 4 && - packet.Name[1].Equal(enc.NewKeywordComponent("DV")) && - packet.Name[2].Equal(enc.NewKeywordComponent("PES")) && - packet.Name[3].Equal(enc.NewKeywordComponent("svs")) - } - var pipeline forwardPipeline var petEntry table.PetEntry var petFound bool @@ -311,12 +293,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // TODO - distinguish fwMulticastEgress pipeline = fwMulticastTransit } else if len(packet.EgressRouter) > 0 { - if isDvSvs { - packet.EgressRouter = nil - petLookup = true - petEntry, petFound = table.Pet.FindLongestPrefixEnc(lookupName) - pipeline = fwMulticastIngress - } else if routerNameSet && packet.EgressRouter.Equal(routerName) { + if routerNameSet && packet.EgressRouter.Equal(routerName) { pipeline = fwUnicastEgress } else { pipeline = fwUnicastTransit @@ -324,12 +301,22 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } else { petLookup = true petEntry, petFound = table.Pet.FindLongestPrefixEnc(lookupName) - if isDvSvs || packet.Name.At(0).Equal(enc.LOCALHOP) || (petFound && petEntry.Multicast) { + // TODO - i think we can remove this isLocalHop? we should be advertising with Multicast in PET for localhop endpoints + if isLocalHop || (petFound && petEntry.Multicast) { pipeline = fwMulticastIngress } else { pipeline = fwUnicastIngress } } + core.Log.Trace(t, "Interest pipeline decision", + "name", packet.Name, + "lookup", lookupName, + "pipeline", pipeline, + "petFound", petFound, + "isLocalHop", isLocalHop, + "egressRouter", len(packet.EgressRouter) > 0, + "bier", len(packet.Bier) > 0, + ) // Get strategy for name. This may not be the final strategy, // we need to get a prelim strategy for CS though for AfterContentStoreHit @@ -421,16 +408,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { petLookup = true } - // drop if no possible egress / local faces found - if !petFound && !strategyName.Equal(defn.BROADCAST_STRATEGY) && !isDvAdvLocalhop && !isDvSvs { - core.Log.Trace(t, "Dropping Interest: no PET entry", - "name", packet.Name, - "lookup", lookupName, - "pipeline", pipeline, - ) - return - } - if petFound { petLocalHops = make([]*table.PetNextHop, 0, len(petEntry.NextHops)) for i := range petEntry.NextHops { @@ -449,23 +426,23 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } } - // nextLocal, nextNet, isMulticast := t.twoPhaseLookup(lookupName, packet.EgressRouter, routerName, routerNameSet, len(packet.Bier) > 0) - localFacesOnly := incomingFace.Scope() != defn.Local && isLocalHop // Unicast Delivery making use of FIB -> FIBStrategy -> unicast delivery if pipeline.isUnicast() { + core.Log.Trace(t, "Unicast pipeline", + "name", packet.Name, + "lookup", lookupName, + "petFound", petFound, + "localHop", isLocalHop, + "localFacesOnly", localFacesOnly, + ) // Deliver to local faces if there exist such legal local faces if len(petLocalHops) > 0 { core.Log.Trace(t, "Local Egress captures the Interest", "name", packet.Name) packet.EgressRouter = nil // TODO: micro-strategy dispatch per PIT in-record t.processOutgoingInterest(packet, pitEntry, petLocalHops[0].FaceID, incomingFace.FaceID()) - // For broadcast-style control traffic (e.g., localhop DV or SVS), - // continue to network forwarding even after local delivery. - if !isLocalHop && !strategyName.Equal(defn.BROADCAST_STRATEGY) { - return - } } // FIB lookup to get the next network hops @@ -486,29 +463,22 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { }) } } - } else if strategyName.Equal(defn.BROADCAST_STRATEGY) || isDvAdvLocalhop { - er := enc.Name{enc.LOCALHOP, enc.NewGenericComponent("neighbors")} - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { - nextNet = append(nextNet, StrategyCandidateHop{ - HopEntry: nextHop, - EgressRouter: er, - }) - } - } else if isDvSvs { + } else { + // Fall back to FIB lookup on the Interest name (or forwarding hint) for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(lookupName) { nextNet = append(nextNet, StrategyCandidateHop{ - HopEntry: nextHop, - EgressRouter: nil, + HopEntry: nextHop, }) } - } else { - core.Log.Trace(t, "Unicast forwarding: no PET and no fallback UNREACHABLE", + } + + if len(nextNet) == 0 { + core.Log.Trace(t, "Unicast forwarding: no PET and no FIB nexthops", "name", packet.Name, "lookup", lookupName, "pipeline", pipeline, "strategy", strategyName, - "isDvAdvLocalhop", isDvAdvLocalhop, - "isDvSvs", isDvSvs, + "isLocalHop", isLocalHop, ) } @@ -538,6 +508,11 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } if len(allowedNetNexthops) > 0 { + core.Log.Trace(t, "Unicast forward", + "name", packet.Name, + "allowedNet", len(allowedNetNexthops), + "localFacesOnly", localFacesOnly, + ) // Forward using a unicast strategy from FibStrategyTable // Below does a FibStrategyTable lookup, but we only have one strategy rn // so we manually use bestRouteStrategy for testing @@ -555,6 +530,13 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // Multicast delivery making use of BIFT -> MulticastStrategyTable -> multicast delivery // Currently just overrides /localhop to a broadcast override and manual BIER code if pipeline.isMulticast() { + core.Log.Trace(t, "Multicast pipeline", + "name", packet.Name, + "lookup", lookupName, + "petFound", petFound, + "localHop", isLocalHop, + "bier", len(packet.Bier), + ) // Deliver to local faces if there exist such legal local faces deliverToLocal := len(petLocalHops) > 0 if deliverToLocal { @@ -565,29 +547,39 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { t.processOutgoingInterest(packet, pitEntry, localHop.FaceID, incomingFace.FaceID()) } if !IsBierEnabled() { - if !isDvSvs && !isLocalHop { + if !isLocalHop { return } } } - if isDvSvs && !isLocalHop { - er := enc.Name{enc.LOCALHOP, enc.NewGenericComponent("neighbors")} - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { - // Exclude incoming face - if nextHop.Nexthop == packet.IncomingFaceID { - continue - } - // Exclude non-local faces for localhop enforcement - if localFacesOnly { - if face := dispatch.GetFace(nextHop.Nexthop); face != nil && face.Scope() != defn.Local { - continue + if petFound { + for _, er := range petEntry.EgressRouters { + if len(er) > 0 && er[0].Equal(enc.LOCALHOP) { + core.Log.Trace(t, "Multicast localhop egress via PET", + "name", packet.Name, + "egress", er, + ) + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + // Enforce localhop: only forward to local faces when incoming is non-local. + if localFacesOnly { + if face := dispatch.GetFace(nextHop.Nexthop); face != nil && face.Scope() != defn.Local { + continue + } + } + // Exclude incoming face + if nextHop.Nexthop == packet.IncomingFaceID { + continue + } + // Exclude faces that have an in-record for this interest + if pitEntry.InRecords()[nextHop.Nexthop] != nil { + continue + } + t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) } + return } - // For DV SVS control, do not suppress forwarding based on PIT in-records. - t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) } - return } if IsBierEnabled() && !isLocalHop { @@ -621,6 +613,10 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } if isLocalHop && !petFound { + core.Log.Trace(t, "Localhop multicast: no PET, forwarding to neighbors", + "name", packet.Name, + "pipeline", pipeline, + ) er := enc.Name{enc.LOCALHOP, enc.NewGenericComponent("neighbors")} for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { // Enforce localhop: only forward to local faces when incoming is non-local. From 70dc41c332259e52e14777dfb1479bcee047123e Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 23 Mar 2026 23:55:25 -0700 Subject: [PATCH 05/38] fw: add string reprs for the pipeline --- fw/fw/thread.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 6eac565e..8e20a2f1 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -33,6 +33,22 @@ const ( fwMulticastTransit ) +var forwardPipelineStrings = map[forwardPipeline]string{ + fwUnicastTransit: "unicast-transit", + fwUnicastIngress: "unicast-ingress", + fwUnicastEgress: "unicast-egress", + fwMulticastIngress: "multicast-ingress", + fwMulticastEgress: "multicast-egress", + fwMulticastTransit: "multicast-transit", +} + +func (p forwardPipeline) String() string { + if s, ok := forwardPipelineStrings[p]; ok { + return s + } + return fmt.Sprintf("forwardPipeline(%d)", int(p)) +} + func (p forwardPipeline) isUnicast() bool { return p == fwUnicastTransit || p == fwUnicastIngress || p == fwUnicastEgress } @@ -302,7 +318,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { petLookup = true petEntry, petFound = table.Pet.FindLongestPrefixEnc(lookupName) // TODO - i think we can remove this isLocalHop? we should be advertising with Multicast in PET for localhop endpoints - if isLocalHop || (petFound && petEntry.Multicast) { + if petFound && petEntry.Multicast { pipeline = fwMulticastIngress } else { pipeline = fwUnicastIngress From 6732a82b409d4ffa7db6fcd9d7526c774165a48a Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 23 Mar 2026 23:57:56 -0700 Subject: [PATCH 06/38] dv: add retry to execMgmt to properly add egress routes for adv svs --- dv/dv/router.go | 54 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/dv/dv/router.go b/dv/dv/router.go index 13d72386..923f27ee 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -285,43 +285,41 @@ func (dv *Router) register() (err error) { } for _, prefix := range pfxs { - dv.nfdc.Exec(nfdc.NfdMgmtCmd{ - Module: "pet", - Cmd: "add-nexthop", - Args: &mgmt.ControlArgs{ - Name: prefix, - }, - Retries: -1, - }) + dv.execMgmtRetry("pet", "add-nexthop", &mgmt.ControlArgs{ + Name: prefix, + }, -1) } // Allow outgoing local-prefix-sync Interests to use two-phase forwarding. // Incoming Interests still terminate locally on the same prefix. // Add multicast flag to the PET announcement - dv.nfdc.Exec(nfdc.NfdMgmtCmd{ - Module: "pet", - Cmd: "add-egress", - Args: &mgmt.ControlArgs{ - Name: dv.pfx.SyncPrefix(), - Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, - Flags: optional.Some(uint64(1)), - }, - Retries: -1, - }) + dv.execMgmtRetry("pet", "add-egress", &mgmt.ControlArgs{ + Name: dv.pfx.SyncPrefix(), + Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, + Flags: optional.Some(uint64(1)), + }, -1) // Set Advertisement Sync to localhop neighbors - dv.nfdc.Exec(nfdc.NfdMgmtCmd{ - Module: "pet", - Cmd: "add-egress", - Args: &mgmt.ControlArgs{ - Name: dv.config.AdvertisementSyncPrefix(), - Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, - Flags: optional.Some(uint64(1)), - }, - Retries: -1, - }) + dv.execMgmtRetry("pet", "add-egress", &mgmt.ControlArgs{ + Name: dv.config.AdvertisementSyncPrefix(), + Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, + Flags: optional.Some(uint64(1)), + }, -1) return nil } +func (dv *Router) execMgmtRetry(module, cmd string, args *mgmt.ControlArgs, retries int) { + for i := 0; i < retries || retries < 0; i++ { + if _, err := dv.engine.ExecMgmtCmd(module, cmd, args); err != nil { + log.Error(dv, "Forwarder command failed", "err", err, "attempt", i, + "module", module, "cmd", cmd, "args", args) + time.Sleep(100 * time.Millisecond) + continue + } + time.Sleep(1 * time.Millisecond) + break + } +} + // createFaces creates faces to all neighbors. func (dv *Router) createFaces() { neighborsPrefix := enc.LOCALHOP.Append(enc.NewGenericComponent("neighbors")) From 9e1d9bef0524c06aec6f174031dbde5f26e37da8 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 24 Mar 2026 00:01:52 -0700 Subject: [PATCH 07/38] fw: remove isLocalHop && !petFound impossible branch --- fw/fw/thread.go | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 8e20a2f1..3be228b4 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -317,7 +317,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } else { petLookup = true petEntry, petFound = table.Pet.FindLongestPrefixEnc(lookupName) - // TODO - i think we can remove this isLocalHop? we should be advertising with Multicast in PET for localhop endpoints if petFound && petEntry.Multicast { pipeline = fwMulticastIngress } else { @@ -628,32 +627,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { return } - if isLocalHop && !petFound { - core.Log.Trace(t, "Localhop multicast: no PET, forwarding to neighbors", - "name", packet.Name, - "pipeline", pipeline, - ) - er := enc.Name{enc.LOCALHOP, enc.NewGenericComponent("neighbors")} - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { - // Enforce localhop: only forward to local faces when incoming is non-local. - if localFacesOnly { - if face := dispatch.GetFace(nextHop.Nexthop); face != nil && face.Scope() != defn.Local { - continue - } - } - // Exclude incoming face - if nextHop.Nexthop == packet.IncomingFaceID { - continue - } - // Exclude faces that have an in-record for this interest - if pitEntry.InRecords()[nextHop.Nexthop] != nil { - continue - } - t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) - } - return - } - if !petFound { core.Log.Trace(t, "localhop, petEntry = nil, UNREACHABLE") return @@ -666,11 +639,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } } return - - // is this needed here or will this be later? - // if BierIsZero(packet.Bier) { - // return - // } } } From 681158b4a305b85a2e798b7d0a9143ad39ecb5a3 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 24 Mar 2026 00:10:04 -0700 Subject: [PATCH 08/38] fw: remove unused twoPhaseLookup fn --- fw/fw/thread.go | 60 +------------------------------------------------ 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 3be228b4..f90f261b 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -568,6 +568,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } } + // TODO - this branch feels weird / not needed if petFound { for _, er := range petEntry.EgressRouters { if len(er) > 0 && er[0].Equal(enc.LOCALHOP) { @@ -652,65 +653,6 @@ func (t *Thread) processBierInterest(packet *defn.Pkt, pitEntry table.PitEntry, }) } -// twoPhaseLookup resolves next-hops for an Interest. -// hasBier indicates that a BIER bit-string is already encoded on the packet; -// in that case the PET→egress mapping is skipped so a transit router does not -// re-query and potentially re-encode a new bit-string. Only the ingress BFIR -// performs the PET egress lookup. -func (t *Thread) twoPhaseLookup(lookupName enc.Name, egressRouter enc.Name, routerName enc.Name, routerNameSet bool, hasBier bool) ( - nextLocal []*table.PetNextHop, - nextNet []StrategyCandidateHop, - isMulticast bool, -) { - // If EgressRouter is set and matches this router, forward to local app faces via PET. - if len(egressRouter) > 0 { - if routerNameSet && egressRouter.Equal(routerName) { - petEntry, petFound := table.Pet.FindLongestPrefixEnc(lookupName) - if petFound { - for i := range petEntry.NextHops { - nextLocal = append(nextLocal, &petEntry.NextHops[i]) - } - } - return nextLocal, nextNet, false - } else { - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(egressRouter) { - nextNet = append(nextNet, StrategyCandidateHop{ - HopEntry: nextHop, - EgressRouter: egressRouter, - }) - } - return nextLocal, nextNet, false - } - } - - // No EgressRouter: try local PET first. - petEntry, petFound := table.Pet.FindLongestPrefixEnc(lookupName) - if !petFound { - return nextLocal, nextNet, false - } - for i := range petEntry.NextHops { - nextLocal = append(nextLocal, &petEntry.NextHops[i]) - } - - // When a BIER bit-string is already present the packet is on a transit router; - // the BIFT + bit-string fully determines forwarding — no PET egress needed. - if hasBier { - return nextLocal, nextNet, false - } - - // Ingress (BFIR) path: map PET egress routers to network next-hops. - for _, er := range petEntry.EgressRouters { - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { - nextNet = append(nextNet, StrategyCandidateHop{ - HopEntry: nextHop, - EgressRouter: er, - }) - } - } - isMulticast = petEntry.Multicast - return nextLocal, nextNet, isMulticast -} - func (t *Thread) processOutgoingInterest( packet *defn.Pkt, pitEntry table.PitEntry, From 42eae17310f5032356a40ac5778b059ea0d48cc7 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 24 Mar 2026 09:48:20 -0700 Subject: [PATCH 09/38] fw: distinguish fwMulticastEgress from fwMulticastTransit if there is BierTag --- fw/fw/thread.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index f90f261b..66e3ce28 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -297,17 +297,17 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { routerName, routerNameSet := CfgRouterName() - // TODO - use an actual table instead of these if statements - // If the first component is /localhop, we do not forward interests received - // on non-local faces to non-local faces isLocalHop := packet.Name.At(0).Equal(enc.LOCALHOP) var pipeline forwardPipeline var petEntry table.PetEntry var petFound bool petLookup := false - if len(packet.Bier) > 0 { - // TODO - distinguish fwMulticastEgress - pipeline = fwMulticastTransit + if IsBierEnabled() && len(packet.Bier) > 0 { + if BierGetBit(BierClone(packet.Bier), CfgBierIndex()) { + pipeline = fwMulticastEgress + } else { + pipeline = fwMulticastTransit + } } else if len(packet.EgressRouter) > 0 { if routerNameSet && packet.EgressRouter.Equal(routerName) { pipeline = fwUnicastEgress From 7847c86bad52b01ae8fb40e88adedd18ca17bf64 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 24 Mar 2026 10:07:43 -0700 Subject: [PATCH 10/38] fw: fix localhop check --- fw/fw/thread.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 66e3ce28..f6bf9729 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -297,7 +297,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { routerName, routerNameSet := CfgRouterName() - isLocalHop := packet.Name.At(0).Equal(enc.LOCALHOP) + isLocalHop := lookupName.At(0).Equal(enc.LOCALHOP) var pipeline forwardPipeline var petEntry table.PetEntry var petFound bool @@ -569,12 +569,13 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } // TODO - this branch feels weird / not needed - if petFound { + if petFound && len(packet.Bier) == 0 && isLocalHop { for _, er := range petEntry.EgressRouters { if len(er) > 0 && er[0].Equal(enc.LOCALHOP) { core.Log.Trace(t, "Multicast localhop egress via PET", "name", packet.Name, "egress", er, + "isLocalHop", isLocalHop, ) for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { // Enforce localhop: only forward to local faces when incoming is non-local. From 046407b6d902d46c4c4c458259d9c54dd178d476 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 30 Mar 2026 22:39:56 -0700 Subject: [PATCH 11/38] fw: revert a check that was breaking multicast --- fw/fw/thread.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index f6bf9729..e98da0e8 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -569,7 +569,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } // TODO - this branch feels weird / not needed - if petFound && len(packet.Bier) == 0 && isLocalHop { + if petFound { for _, er := range petEntry.EgressRouters { if len(er) > 0 && er[0].Equal(enc.LOCALHOP) { core.Log.Trace(t, "Multicast localhop egress via PET", From cc287f00e42886d2731c4b23618626e6ff297df4 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 30 Mar 2026 23:22:07 -0700 Subject: [PATCH 12/38] fw: some comments --- fw/fw/broadcast.go | 85 ---------------------------------------------- fw/fw/thread.go | 13 ++++--- 2 files changed, 8 insertions(+), 90 deletions(-) delete mode 100644 fw/fw/broadcast.go diff --git a/fw/fw/broadcast.go b/fw/fw/broadcast.go deleted file mode 100644 index 338ee270..00000000 --- a/fw/fw/broadcast.go +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Broadcast forwarding strategy. - * - * Using this strategy, the interest packet will be forwarded to all nexthops. - */ - -package fw - -import ( - "github.com/named-data/ndnd/fw/core" - "github.com/named-data/ndnd/fw/defn" - "github.com/named-data/ndnd/fw/table" -) - -// The strategy to forward interests to all nexthops. -type Broadcast struct { - StrategyBase -} - -// (AI GENERATED DESCRIPTION): Registers the Broadcast strategy with version 1 in the strategy registry by appending its constructor to the init list. -func init() { - strategyInit = append(strategyInit, func() Strategy { return &Broadcast{} }) - StrategyVersions["broadcast"] = []uint64{1} -} - -// (AI GENERATED DESCRIPTION): Initializes the *Broadcast strategy by setting up its base with the name “broadcast” and priority 1 on the provided forwarding thread. -func (s *Broadcast) Instantiate(fwThread *Thread) { - s.NewStrategyBase(fwThread, "broadcast", 1) -} - -// (AI GENERATED DESCRIPTION): Sends a cached Data packet (retrieved from the Content Store) back to the requester via the specified PIT entry, using the requesting face and indicating the Content Store as the data source. -func (s *Broadcast) AfterContentStoreHit( - packet *defn.Pkt, - pitEntry table.PitEntry, - inFace uint64, -) { - core.Log.Trace(s, "AfterContentStoreHit", "name", packet.Name, "faceid", inFace) - s.SendData(packet, pitEntry, inFace, 0) // 0 indicates ContentStore is source -} - -// (AI GENERATED DESCRIPTION): Forwards a received Data packet to every face recorded in the PIT entry, logging each forwarding step and invoking SendData for each destination. -func (s *Broadcast) AfterReceiveData( - packet *defn.Pkt, - pitEntry table.PitEntry, - inFace uint64, -) { - core.Log.Trace(s, "AfterReceiveData", "name", packet.Name, "inrecords", len(pitEntry.InRecords())) - for faceID := range pitEntry.InRecords() { - core.Log.Trace(s, "Forwarding Data", "name", packet.Name, "faceid", faceID) - s.SendData(packet, pitEntry, faceID, inFace) - } -} - -// Forwards an incoming Interest to all next-hops -func (s *Broadcast) AfterReceiveInterest( - packet *defn.Pkt, - pitEntry table.PitEntry, - inFace uint64, - nexthops []StrategyCandidateHop, -) { - if len(nexthops) == 0 { - core.Log.Debug(s, "No nexthop found - DROP", "name", packet.Name) - return - } - - successfulForward := false - - for _, nh := range nexthops { - if sent := s.SendInterest(packet, pitEntry, nh.HopEntry.Nexthop, inFace); sent { - core.Log.Trace(s, "Forwarded Interest", "name", packet.Name, "faceid", nh.HopEntry.Nexthop) - successfulForward = true - } else { - core.Log.Trace(s, "Error forwarding interest", "name", packet.Name, "faceid", nh.HopEntry.Nexthop) - } - } - - if !successfulForward { - core.Log.Debug(s, "No usable nexthop for Interest - DROP", "name", packet.Name) - } -} - -// (AI GENERATED DESCRIPTION): No‑op hook invoked before satisfying an Interest in the Broadcast strategy – it performs no action. -func (s *Broadcast) BeforeSatisfyInterest(pitEntry table.PitEntry, inFace uint64) { - // This does nothing in Broadcast -} diff --git a/fw/fw/thread.go b/fw/fw/thread.go index e98da0e8..b677060a 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -561,14 +561,17 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { packet.EgressRouter = nil t.processOutgoingInterest(packet, pitEntry, localHop.FaceID, incomingFace.FaceID()) } - if !IsBierEnabled() { - if !isLocalHop { - return - } - } + // This branch was only there for this hard-coded version. disregard it + // if !IsBierEnabled() { + // if !isLocalHop { + // return + // } + // } } // TODO - this branch feels weird / not needed + // this branch is only currently needed for all the routes with + // egress of /localhop/neighbors to broadcast to them if petFound { for _, er := range petEntry.EgressRouters { if len(er) > 0 && er[0].Equal(enc.LOCALHOP) { From 0302bc44cdecbee064e0134b4ce4bb465769cd48 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 30 Mar 2026 23:56:42 -0700 Subject: [PATCH 13/38] fw: implement table to identify multicast forwarding strategy --- dv/dv/router.go | 13 ++ fw/defn/name.go | 4 + fw/fw/thread.go | 72 +++++----- fw/mgmt/multicast-strategy-choice.go | 188 +++++++++++++++++++++++++++ fw/mgmt/thread.go | 1 + fw/table/config.go | 15 +++ fw/table/fib-strategy-hashtable.go | 33 +++-- fw/table/fib-strategy-tree.go | 23 +++- fw/table/fib-strategy.go | 4 + 9 files changed, 303 insertions(+), 50 deletions(-) create mode 100644 fw/mgmt/multicast-strategy-choice.go diff --git a/dv/dv/router.go b/dv/dv/router.go index 923f27ee..ea8fb9ba 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -8,6 +8,7 @@ import ( "github.com/named-data/ndnd/dv/config" "github.com/named-data/ndnd/dv/nfdc" "github.com/named-data/ndnd/dv/table" + "github.com/named-data/ndnd/fw/defn" enc "github.com/named-data/ndnd/std/encoding" "github.com/named-data/ndnd/std/log" "github.com/named-data/ndnd/std/ndn" @@ -304,6 +305,18 @@ func (dv *Router) register() (err error) { Flags: optional.Some(uint64(1)), }, -1) + // Force multicast strategy for sync prefixes to broadcast. + broadcastPrefixes := []enc.Name{ + dv.pfx.SyncPrefix(), + dv.config.AdvertisementSyncPrefix(), + } + for _, prefix := range broadcastPrefixes { + dv.execMgmtRetry("multicast-strategy-choice", "set", &mgmt.ControlArgs{ + Name: prefix, + Strategy: &mgmt.Strategy{Name: defn.BROADCAST_STRATEGY}, + }, -1) + } + return nil } diff --git a/fw/defn/name.go b/fw/defn/name.go index a3ea09d6..a1190702 100644 --- a/fw/defn/name.go +++ b/fw/defn/name.go @@ -19,5 +19,9 @@ var BROADCAST_STRATEGY = STRATEGY_PREFIX. Append(enc.NewGenericComponent("broadcast")). Append(enc.NewVersionComponent(1)) +var BIER_STRATEGY = STRATEGY_PREFIX. + Append(enc.NewGenericComponent("bier")). + Append(enc.NewVersionComponent(1)) + // Default forwarding strategy name var DEFAULT_STRATEGY = BEST_ROUTE_STRATEGY diff --git a/fw/fw/thread.go b/fw/fw/thread.go index b677060a..7f145e99 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -545,12 +545,43 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // Multicast delivery making use of BIFT -> MulticastStrategyTable -> multicast delivery // Currently just overrides /localhop to a broadcast override and manual BIER code if pipeline.isMulticast() { + multicastStrategyName := table.MulticastStrategyTable.FindStrategyEnc(interest.Name()) + useBier := multicastStrategyName.Equal(defn.BIER_STRATEGY) + useBroadcast := multicastStrategyName.Equal(defn.BROADCAST_STRATEGY) + if !useBier && !useBroadcast { + core.Log.Warn(t, "Unknown multicast strategy, defaulting to broadcast", + "name", packet.Name, + "strategy", multicastStrategyName, + ) + useBroadcast = true + } + if useBier && !IsBierEnabled() { + core.Log.Warn(t, "BIER strategy selected but BIER is disabled; falling back to broadcast", + "name", packet.Name, + "strategy", multicastStrategyName, + ) + useBier = false + useBroadcast = true + } + // This scenario can happen but not to worry as it is just a temporary state + // if useBier && isLocalHop { + // core.Log.Debug(t, "BIER strategy selected for localhop; falling back to broadcast", + // "name", packet.Name, + // "strategy", multicastStrategyName, + // ) + // useBier = false + // useBroadcast = true + // } + core.Log.Trace(t, "Multicast pipeline", "name", packet.Name, "lookup", lookupName, "petFound", petFound, "localHop", isLocalHop, "bier", len(packet.Bier), + "mcastStrategy", multicastStrategyName, + "useBier", useBier, + "useBroadcast", useBroadcast, ) // Deliver to local faces if there exist such legal local faces deliverToLocal := len(petLocalHops) > 0 @@ -561,7 +592,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { packet.EgressRouter = nil t.processOutgoingInterest(packet, pitEntry, localHop.FaceID, incomingFace.FaceID()) } - // This branch was only there for this hard-coded version. disregard it + // This branch was only there for this hard-coded version. disregard it // if !IsBierEnabled() { // if !isLocalHop { // return @@ -569,44 +600,12 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // } } - // TODO - this branch feels weird / not needed - // this branch is only currently needed for all the routes with - // egress of /localhop/neighbors to broadcast to them - if petFound { - for _, er := range petEntry.EgressRouters { - if len(er) > 0 && er[0].Equal(enc.LOCALHOP) { - core.Log.Trace(t, "Multicast localhop egress via PET", - "name", packet.Name, - "egress", er, - "isLocalHop", isLocalHop, - ) - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { - // Enforce localhop: only forward to local faces when incoming is non-local. - if localFacesOnly { - if face := dispatch.GetFace(nextHop.Nexthop); face != nil && face.Scope() != defn.Local { - continue - } - } - // Exclude incoming face - if nextHop.Nexthop == packet.IncomingFaceID { - continue - } - // Exclude faces that have an in-record for this interest - if pitEntry.InRecords()[nextHop.Nexthop] != nil { - continue - } - t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) - } - return - } - } - } - - if IsBierEnabled() && !isLocalHop { + if useBier { core.Log.Trace(t, "Multicast BIER path", "name", packet.Name, "bier", len(packet.Bier), "egressRouters", len(petEntry.EgressRouters), + "strategy", multicastStrategyName, ) if len(packet.Bier) == 0 { if pipeline != fwMulticastIngress { @@ -640,6 +639,9 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // Can we just replace this with the actual adjacent faces? for _, er := range petEntry.EgressRouters { for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + if pitEntry.InRecords()[nextHop.Nexthop] != nil { + continue + } t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) } } diff --git a/fw/mgmt/multicast-strategy-choice.go b/fw/mgmt/multicast-strategy-choice.go new file mode 100644 index 00000000..d66ed775 --- /dev/null +++ b/fw/mgmt/multicast-strategy-choice.go @@ -0,0 +1,188 @@ +/* YaNFD - Yet another NDN Forwarding Daemon + * + * Copyright (C) 2026 + * + * This file is licensed under the terms of the MIT License, as found in LICENSE.md. + */ + +package mgmt + +import ( + "github.com/named-data/ndnd/fw/core" + "github.com/named-data/ndnd/fw/defn" + "github.com/named-data/ndnd/fw/table" + enc "github.com/named-data/ndnd/std/encoding" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" +) + +// MulticastStrategyChoiceModule handles multicast strategy choice management. +type MulticastStrategyChoiceModule struct { + manager *Thread +} + +// (AI GENERATED DESCRIPTION): Returns the identifier string "mgmt-multicast-strategy" for the module. +func (s *MulticastStrategyChoiceModule) String() string { + return "mgmt-multicast-strategy" +} + +// (AI GENERATED DESCRIPTION): Registers the specified manager by assigning it to the module's manager field. +func (s *MulticastStrategyChoiceModule) registerManager(manager *Thread) { + s.manager = manager +} + +// (AI GENERATED DESCRIPTION): Returns the manager thread associated with this module. +func (s *MulticastStrategyChoiceModule) getManager() *Thread { + return s.manager +} + +// (AI GENERATED DESCRIPTION): Handles incoming multicast strategy management Interests. +func (s *MulticastStrategyChoiceModule) handleIncomingInterest(interest *Interest) { + // Only allow from /localhost + if !LOCAL_PREFIX.IsPrefix(interest.Name()) { + core.Log.Warn(s, "Received multicast strategy management Interest from non-local source - DROP") + return + } + + verb := interest.Name()[len(LOCAL_PREFIX)+1].String() + switch verb { + case "set": + s.set(interest) + case "unset": + s.unset(interest) + case "list": + s.list(interest) + default: + s.manager.sendCtrlResp(interest, 501, "Unknown verb", nil) + return + } +} + +func normalizeMulticastStrategy(name enc.Name) (enc.Name, bool) { + if !defn.STRATEGY_PREFIX.IsPrefix(name) { + return nil, false + } + + if len(name) == len(defn.STRATEGY_PREFIX)+1 { + strategyName := name[len(defn.STRATEGY_PREFIX)].String() + switch strategyName { + case "broadcast": + return defn.BROADCAST_STRATEGY, true + case "bier": + return defn.BIER_STRATEGY, true + default: + return nil, false + } + } + + if len(name) == len(defn.STRATEGY_PREFIX)+2 && name[len(defn.STRATEGY_PREFIX)+1].IsVersion() { + strategyName := name[len(defn.STRATEGY_PREFIX)].String() + versionBytes := name[len(defn.STRATEGY_PREFIX)+1].Val + version, _, err := enc.ParseNat(versionBytes) + if err != nil || version != 1 { + return nil, false + } + switch strategyName { + case "broadcast": + return defn.BROADCAST_STRATEGY, true + case "bier": + return defn.BIER_STRATEGY, true + default: + return nil, false + } + } + + return nil, false +} + +// (AI GENERATED DESCRIPTION): Handles a SetStrategy control request for multicast strategies. +func (s *MulticastStrategyChoiceModule) set(interest *Interest) { + if len(interest.Name()) < len(LOCAL_PREFIX)+3 { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) + return + } + + params := decodeControlParameters(s, interest) + if params == nil { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) + return + } + + if params.Name == nil { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (missing Name)", nil) + return + } + + if params.Strategy == nil { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (missing Strategy)", nil) + return + } + + normalized, ok := normalizeMulticastStrategy(params.Strategy.Name) + if !ok { + core.Log.Warn(s, "Invalid multicast strategy", "strategy", params.Strategy.Name) + s.manager.sendCtrlResp(interest, 404, "Invalid multicast strategy", nil) + return + } + + table.MulticastStrategyTable.SetStrategyEnc(params.Name, normalized) + + s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{ + Name: params.Name, + Strategy: &mgmt.Strategy{Name: normalized}, + }) + + core.Log.Info(s, "Set multicast strategy", "name", params.Name, "strategy", normalized) +} + +// (AI GENERATED DESCRIPTION): Unsets a multicast strategy for a given name. +func (s *MulticastStrategyChoiceModule) unset(interest *Interest) { + if len(interest.Name()) < len(LOCAL_PREFIX)+3 { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) + return + } + + params := decodeControlParameters(s, interest) + if params == nil { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) + return + } + + if params.Name == nil { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (missing Name)", nil) + return + } + + if len(params.Name) == 0 { + s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (empty Name)", nil) + return + } + + table.MulticastStrategyTable.UnSetStrategyEnc(params.Name) + core.Log.Info(s, "Unset multicast strategy", "name", params.Name) + + s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{Name: params.Name}) +} + +// (AI GENERATED DESCRIPTION): Handles listing multicast strategies. +func (s *MulticastStrategyChoiceModule) list(interest *Interest) { + if len(interest.Name()) > len(LOCAL_PREFIX)+2 { + // Ignore because contains version and/or segment components + return + } + + entries := table.MulticastStrategyTable.GetAllForwardingStrategies() + choices := []*mgmt.StrategyChoice{} + for _, fsEntry := range entries { + choices = append(choices, &mgmt.StrategyChoice{ + Name: fsEntry.Name(), + Strategy: &mgmt.Strategy{Name: fsEntry.GetStrategy()}, + }) + } + dataset := &mgmt.StrategyChoiceMsg{StrategyChoices: choices} + + name := LOCAL_PREFIX.Append( + enc.NewGenericComponent("multicast-strategy-choice"), + enc.NewGenericComponent("list"), + ) + s.manager.sendStatusDataset(interest, name, dataset.Encode()) +} diff --git a/fw/mgmt/thread.go b/fw/mgmt/thread.go index 1f27f91c..6b25b209 100644 --- a/fw/mgmt/thread.go +++ b/fw/mgmt/thread.go @@ -64,6 +64,7 @@ func MakeMgmtThread() *Thread { m.registerModule("rib", new(RIBModule)) m.registerModule("status", new(ForwarderStatusModule)) m.registerModule("strategy-choice", new(StrategyChoiceModule)) + m.registerModule("multicast-strategy-choice", new(MulticastStrategyChoiceModule)) // readvertisers run in the management thread for ease of // implementation, since they use the internal transport diff --git a/fw/table/config.go b/fw/table/config.go index 92b683c6..e2d7fda7 100644 --- a/fw/table/config.go +++ b/fw/table/config.go @@ -12,6 +12,7 @@ import ( "time" "github.com/named-data/ndnd/fw/core" + "github.com/named-data/ndnd/fw/defn" enc "github.com/named-data/ndnd/std/encoding" ) @@ -39,6 +40,20 @@ func Initialize() { core.Log.Fatal(nil, "Unknown FIB table algorithm", "algo", core.C.Tables.Fib.Algorithm) } + // Create Multicast strategy table + defaultMulticastStrategy := defn.BROADCAST_STRATEGY + if core.C.Fw.BierIndex >= 0 { + defaultMulticastStrategy = defn.BIER_STRATEGY + } + switch core.C.Tables.Fib.Algorithm { + case "hashtable": + newMulticastStrategyTableHashTable(core.C.Tables.Fib.Hashtable.M, defaultMulticastStrategy) + case "nametree": + newMulticastStrategyTableTree(defaultMulticastStrategy) + default: + core.Log.Fatal(nil, "Unknown FIB table algorithm", "algo", core.C.Tables.Fib.Algorithm) + } + // Create Network Region Table for _, region := range core.C.Tables.NetworkRegion.Regions { name, err := enc.NameFromStr(region) diff --git a/fw/table/fib-strategy-hashtable.go b/fw/table/fib-strategy-hashtable.go index afadb021..4f6a4f35 100644 --- a/fw/table/fib-strategy-hashtable.go +++ b/fw/table/fib-strategy-hashtable.go @@ -50,21 +50,34 @@ type FibStrategyHashTable struct { fibStrategyRWMutex sync.RWMutex } -// newFibStrategyTableHashTable creates a new FIB with the hash table algorithm. +// newStrategyTableHashTable creates a new strategy table with the hash table algorithm. // The argument m determines the virtual name length. -func newFibStrategyTableHashTable(m uint16) { - FibStrategyTable = new(FibStrategyHashTable) - fibStrategyTableHashTable := FibStrategyTable.(*FibStrategyHashTable) +func newStrategyTableHashTable(m uint16, defaultStrategy enc.Name) *FibStrategyHashTable { + table := new(FibStrategyHashTable) - fibStrategyTableHashTable.m = int(m) // Cast to int so that it's easy to pass to name.Prefix - fibStrategyTableHashTable.realTable = make(map[uint64]*baseFibStrategyEntry) - fibStrategyTableHashTable.virtTable = make(map[uint64]*virtualDetails) - fibStrategyTableHashTable.virtTableNames = make(map[uint64]map[string]int) + table.m = int(m) // Cast to int so that it's easy to pass to name.Prefix + table.realTable = make(map[uint64]*baseFibStrategyEntry) + table.virtTable = make(map[uint64]*virtualDetails) + table.virtTableNames = make(map[uint64]map[string]int) rtEntry := new(baseFibStrategyEntry) rtEntry.name = enc.Name{} - rtEntry.strategy = defn.DEFAULT_STRATEGY - fibStrategyTableHashTable.realTable[enc.Name{}.Hash()] = rtEntry + rtEntry.strategy = defaultStrategy + table.realTable[enc.Name{}.Hash()] = rtEntry + + return table +} + +// newFibStrategyTableHashTable creates a new FIB with the hash table algorithm. +// The argument m determines the virtual name length. +func newFibStrategyTableHashTable(m uint16) { + FibStrategyTable = newStrategyTableHashTable(m, defn.DEFAULT_STRATEGY) +} + +// newMulticastStrategyTableHashTable creates a new multicast strategy table +// with the hash table algorithm. The argument m determines the virtual name length. +func newMulticastStrategyTableHashTable(m uint16, defaultStrategy enc.Name) { + MulticastStrategyTable = newStrategyTableHashTable(m, defaultStrategy) } // findLongestPrefixMatch returns the entry corresponding to the longest diff --git a/fw/table/fib-strategy-tree.go b/fw/table/fib-strategy-tree.go index 6bc01411..7752c045 100644 --- a/fw/table/fib-strategy-tree.go +++ b/fw/table/fib-strategy-tree.go @@ -32,16 +32,29 @@ type FibStrategyTree struct { mutex sync.RWMutex } -// (AI GENERATED DESCRIPTION): Initializes the FibStrategyTable with a new FibStrategyTree whose root entry has an empty component, default strategy, and empty name. -func newFibStrategyTableTree() { - FibStrategyTable = new(FibStrategyTree) - tree := FibStrategyTable.(*FibStrategyTree) +// newStrategyTableTree initializes a new strategy table tree whose root entry +// has an empty component, default strategy, and empty name. +func newStrategyTableTree(defaultStrategy enc.Name) *FibStrategyTree { + tree := new(FibStrategyTree) // Root component will be empty tree.root = new(fibStrategyTreeEntry) tree.root.component = enc.Component{} - tree.root.strategy = defn.DEFAULT_STRATEGY + tree.root.strategy = defaultStrategy tree.root.name = enc.Name{} + + return tree +} + +// (AI GENERATED DESCRIPTION): Initializes the FibStrategyTable with a new FibStrategyTree whose root entry has an empty component, default strategy, and empty name. +func newFibStrategyTableTree() { + FibStrategyTable = newStrategyTableTree(defn.DEFAULT_STRATEGY) +} + +// newMulticastStrategyTableTree initializes the MulticastStrategyTable with +// a new FibStrategyTree whose root entry has an empty component and default strategy. +func newMulticastStrategyTableTree(defaultStrategy enc.Name) { + MulticastStrategyTable = newStrategyTableTree(defaultStrategy) } // findExactMatchEntry returns the entry corresponding to the exact match of diff --git a/fw/table/fib-strategy.go b/fw/table/fib-strategy.go index f80db8f1..61ba6260 100644 --- a/fw/table/fib-strategy.go +++ b/fw/table/fib-strategy.go @@ -50,6 +50,10 @@ type FibStrategy interface { // FibStrategy is a table containing FIB and Strategy entries for given prefixes. var FibStrategyTable FibStrategy +// MulticastStrategyTable is a table containing multicast strategy entries for given prefixes. +// It uses the same API as FibStrategyTable but only stores multicast strategies. +var MulticastStrategyTable FibStrategy + // Name returns the name associated with the baseFibStrategyEntry. func (e *baseFibStrategyEntry) Name() enc.Name { return e.name From da41f1668962f1a86509f8792d9a8fdef91eb1a1 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 31 Mar 2026 00:44:56 -0700 Subject: [PATCH 14/38] fw: refactor multicast into a strategy dispatch table --- fw/fw/bestroute.go | 14 ++++++ fw/fw/bier_strategy.go | 49 +++++++++++++++++-- fw/fw/broadcast_strategy.go | 97 +++++++++++++++++++++++++++++++++++++ fw/fw/multicast.go | 14 ++++++ fw/fw/replicast.go | 14 ++++++ fw/fw/strategy.go | 5 ++ fw/fw/thread.go | 95 ++---------------------------------- 7 files changed, 193 insertions(+), 95 deletions(-) create mode 100644 fw/fw/broadcast_strategy.go diff --git a/fw/fw/bestroute.go b/fw/fw/bestroute.go index 03b697be..d3386a97 100644 --- a/fw/fw/bestroute.go +++ b/fw/fw/bestroute.go @@ -117,6 +117,20 @@ func (s *BestRoute) AfterReceiveInterest( core.Log.Debug(s, "No usable nexthop for Interest - DROP", "name", packet.Name) } +func (s *BestRoute) AfterReceiveMulticastInterest( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, + petEntry table.PetEntry, +) { + core.Log.Error(s, "BestRoute does not support AfterReceiveMulticastInterest", + "name", packet.Name, + "inFace", inFace, + "petNextHops", len(petEntry.NextHops), + "petEgress", len(petEntry.EgressRouters), + ) +} + // (AI GENERATED DESCRIPTION): No‑op; the BestRoute strategy performs no action before satisfying an Interest. func (s *BestRoute) BeforeSatisfyInterest(pitEntry table.PitEntry, inFace uint64) { // This does nothing in BestRoute diff --git a/fw/fw/bier_strategy.go b/fw/fw/bier_strategy.go index 310dc8eb..5c6f49dc 100644 --- a/fw/fw/bier_strategy.go +++ b/fw/fw/bier_strategy.go @@ -74,11 +74,54 @@ func (s *BierStrategy) AfterReceiveInterest( inFace uint64, nexthops []StrategyCandidateHop, ) { + core.Log.Error(s, "BierStrategy does not support AfterReceiveInterest (unicast)", + "name", packet.Name, + "inFace", inFace, + "nexthops", len(nexthops), + ) +} + +func (s *BierStrategy) AfterReceiveMulticastInterest( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, + petEntry table.PetEntry, +) { + // Deliver to local faces if there exist such legal local faces + deliverToLocal := false + if len(petEntry.NextHops) > 0 { + for i := range petEntry.NextHops { + nexthop := petEntry.NextHops[i] + if nexthop.FaceID == inFace { + continue + } + if pitEntry.InRecords()[nexthop.FaceID] != nil { + continue + } + deliverToLocal = true + packet.EgressRouter = nil + s.SendInterest(packet, pitEntry, nexthop.FaceID, inFace) + } + } + if len(packet.Bier) == 0 { - // BierStrategy only handles BIER Interests. No broadcast fallback. - core.Log.Debug(s, "BierStrategy: no bit-string, dropping", "name", packet.Name) - return + if len(petEntry.EgressRouters) == 0 { + core.Log.Trace(s, "Multicast BIER empty without PET egress; drop", "name", packet.Name) + return + } + core.Log.Trace(s, "BFIR: encoding BIER bit-string", "name", packet.Name, "egress-count", len(petEntry.EgressRouters)) + packet.Bier = Bift.BuildBierBitString(petEntry.EgressRouters) } + + if deliverToLocal && len(packet.Bier) > 0 && IsBierEnabled() { + bs := BierClone(packet.Bier) + BierClearBit(bs, CfgBierIndex()) + packet.Bier = bs + if BierIsZero(bs) { + return + } + } + s.replicateBier(packet, pitEntry, inFace) } diff --git a/fw/fw/broadcast_strategy.go b/fw/fw/broadcast_strategy.go new file mode 100644 index 00000000..d2fcc11f --- /dev/null +++ b/fw/fw/broadcast_strategy.go @@ -0,0 +1,97 @@ +/* Broadcast Strategy for ndnd + * + * Implements multicast broadcast forwarding as a proper NDN forwarding strategy. + */ + +package fw + +import ( + "github.com/named-data/ndnd/fw/core" + "github.com/named-data/ndnd/fw/defn" + "github.com/named-data/ndnd/fw/table" +) + +// BroadcastStrategy implements broadcast multicast forwarding. +type BroadcastStrategy struct { + StrategyBase +} + +func init() { + strategyInit = append(strategyInit, func() Strategy { return &BroadcastStrategy{} }) + StrategyVersions["broadcast"] = []uint64{1} +} + +func (s *BroadcastStrategy) Instantiate(fwThread *Thread) { + s.NewStrategyBase(fwThread, "broadcast", 1) +} + +func (s *BroadcastStrategy) AfterContentStoreHit( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, +) { + core.Log.Trace(s, "AfterContentStoreHit", "name", packet.Name, "faceid", inFace) + s.SendData(packet, pitEntry, inFace, 0) +} + +func (s *BroadcastStrategy) AfterReceiveData( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, +) { + core.Log.Trace(s, "AfterReceiveData", "name", packet.Name, "inrecords", len(pitEntry.InRecords())) + for faceID := range pitEntry.InRecords() { + core.Log.Trace(s, "Forwarding Data", "name", packet.Name, "faceid", faceID) + s.SendData(packet, pitEntry, faceID, inFace) + } +} + +func (s *BroadcastStrategy) AfterReceiveInterest( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, + nexthops []StrategyCandidateHop, +) { + core.Log.Error(s, "BroadcastStrategy does not support AfterReceiveInterest (unicast)", + "name", packet.Name, + "inFace", inFace, + "nexthops", len(nexthops), + ) +} + +func (s *BroadcastStrategy) AfterReceiveMulticastInterest( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, + petEntry table.PetEntry, +) { + // Deliver to local faces if there exist such legal local faces + if len(petEntry.NextHops) > 0 { + for i := range petEntry.NextHops { + nexthop := petEntry.NextHops[i] + if nexthop.FaceID == inFace { + continue + } + if pitEntry.InRecords()[nexthop.FaceID] != nil { + continue + } + packet.EgressRouter = nil + s.SendInterest(packet, pitEntry, nexthop.FaceID, inFace) + } + } + + if len(petEntry.EgressRouters) == 0 { + core.Log.Trace(s, "Broadcast without PET egress routers; drop", "name", packet.Name) + return + } + for _, er := range petEntry.EgressRouters { + for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + if pitEntry.InRecords()[nextHop.Nexthop] != nil { + continue + } + s.SendInterest(packet, pitEntry, nextHop.Nexthop, inFace) + } + } +} + +func (s *BroadcastStrategy) BeforeSatisfyInterest(pitEntry table.PitEntry, inFace uint64) {} diff --git a/fw/fw/multicast.go b/fw/fw/multicast.go index 8030ff2f..cc0147ae 100644 --- a/fw/fw/multicast.go +++ b/fw/fw/multicast.go @@ -90,6 +90,20 @@ func (s *Multicast) AfterReceiveInterest( } } +func (s *Multicast) AfterReceiveMulticastInterest( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, + petEntry table.PetEntry, +) { + core.Log.Error(s, "Multicast does not support AfterReceiveMulticastInterest", + "name", packet.Name, + "inFace", inFace, + "petNextHops", len(petEntry.NextHops), + "petEgress", len(petEntry.EgressRouters), + ) +} + // (AI GENERATED DESCRIPTION): No‑op hook invoked before satisfying an Interest in the Multicast strategy – it performs no action. func (s *Multicast) BeforeSatisfyInterest(pitEntry table.PitEntry, inFace uint64) { // This does nothing in Multicast diff --git a/fw/fw/replicast.go b/fw/fw/replicast.go index 6c3d6414..7927331d 100644 --- a/fw/fw/replicast.go +++ b/fw/fw/replicast.go @@ -102,6 +102,20 @@ func (s *Replicast) AfterReceiveInterest( } } +func (s *Replicast) AfterReceiveMulticastInterest( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, + petEntry table.PetEntry, +) { + core.Log.Error(s, "Replicast does not support AfterReceiveMulticastInterest", + "name", packet.Name, + "inFace", inFace, + "petNextHops", len(petEntry.NextHops), + "petEgress", len(petEntry.EgressRouters), + ) +} + // (AI GENERATED DESCRIPTION): No‑op hook invoked before satisfying an Interest in the Replicast strategy – it performs no action. func (s *Replicast) BeforeSatisfyInterest(pitEntry table.PitEntry, inFace uint64) { // This does nothing in Replicast diff --git a/fw/fw/strategy.go b/fw/fw/strategy.go index 1546539b..570afe92 100644 --- a/fw/fw/strategy.go +++ b/fw/fw/strategy.go @@ -42,6 +42,11 @@ type Strategy interface { pitEntry table.PitEntry, inFace uint64, nexthops []StrategyCandidateHop) + AfterReceiveMulticastInterest( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, + petEntry table.PetEntry) BeforeSatisfyInterest( pitEntry table.PitEntry, inFace uint64) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 7f145e99..4cf61e56 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -545,34 +545,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { // Multicast delivery making use of BIFT -> MulticastStrategyTable -> multicast delivery // Currently just overrides /localhop to a broadcast override and manual BIER code if pipeline.isMulticast() { - multicastStrategyName := table.MulticastStrategyTable.FindStrategyEnc(interest.Name()) - useBier := multicastStrategyName.Equal(defn.BIER_STRATEGY) - useBroadcast := multicastStrategyName.Equal(defn.BROADCAST_STRATEGY) - if !useBier && !useBroadcast { - core.Log.Warn(t, "Unknown multicast strategy, defaulting to broadcast", - "name", packet.Name, - "strategy", multicastStrategyName, - ) - useBroadcast = true - } - if useBier && !IsBierEnabled() { - core.Log.Warn(t, "BIER strategy selected but BIER is disabled; falling back to broadcast", - "name", packet.Name, - "strategy", multicastStrategyName, - ) - useBier = false - useBroadcast = true - } - // This scenario can happen but not to worry as it is just a temporary state - // if useBier && isLocalHop { - // core.Log.Debug(t, "BIER strategy selected for localhop; falling back to broadcast", - // "name", packet.Name, - // "strategy", multicastStrategyName, - // ) - // useBier = false - // useBroadcast = true - // } - + multicastStrategyName := table.MulticastStrategyTable.FindStrategyEnc(lookupName) core.Log.Trace(t, "Multicast pipeline", "name", packet.Name, "lookup", lookupName, @@ -580,71 +553,9 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { "localHop", isLocalHop, "bier", len(packet.Bier), "mcastStrategy", multicastStrategyName, - "useBier", useBier, - "useBroadcast", useBroadcast, ) - // Deliver to local faces if there exist such legal local faces - deliverToLocal := len(petLocalHops) > 0 - if deliverToLocal { - core.Log.Trace(t, "Local Egress captures the Interest", "name", packet.Name) - // In multicast, we deliver to all local faces unlike in unicast - for _, localHop := range petLocalHops { - packet.EgressRouter = nil - t.processOutgoingInterest(packet, pitEntry, localHop.FaceID, incomingFace.FaceID()) - } - // This branch was only there for this hard-coded version. disregard it - // if !IsBierEnabled() { - // if !isLocalHop { - // return - // } - // } - } - - if useBier { - core.Log.Trace(t, "Multicast BIER path", - "name", packet.Name, - "bier", len(packet.Bier), - "egressRouters", len(petEntry.EgressRouters), - "strategy", multicastStrategyName, - ) - if len(packet.Bier) == 0 { - if pipeline != fwMulticastIngress { - core.Log.Trace(t, "Multicast BIER empty, non-ingress UNREACHABLE", - "name", packet.Name, - "pipeline", pipeline, - ) - return - } - core.Log.Trace(t, "BFIR: encoding BIER bit-string", "name", packet.Name, "egress-count", len(petEntry.EgressRouters)) - packet.Bier = Bift.BuildBierBitString(petEntry.EgressRouters) - } - if deliverToLocal && len(packet.Bier) > 0 && IsBierEnabled() { - bs := BierClone(packet.Bier) - BierClearBit(bs, CfgBierIndex()) - packet.Bier = bs - if BierIsZero(bs) { - return - } - } - - t.processBierInterest(packet, pitEntry, incomingFace.FaceID()) - return - } - - if !petFound { - core.Log.Trace(t, "localhop, petEntry = nil, UNREACHABLE") - return - } - // TODO - why do we need to do a FIB lookup in this case? makes no sense? - // Can we just replace this with the actual adjacent faces? - for _, er := range petEntry.EgressRouters { - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { - if pitEntry.InRecords()[nextHop.Nexthop] != nil { - continue - } - t.processOutgoingInterest(packet, pitEntry, nextHop.Nexthop, incomingFace.FaceID()) - } - } + strategy := t.strategies[multicastStrategyName.Hash()] + strategy.AfterReceiveMulticastInterest(packet, pitEntry, incomingFace.FaceID(), petEntry) return } } From 08d4d0e00f7b22b070c390e4da2f9e2c6c663d79 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 31 Mar 2026 00:45:17 -0700 Subject: [PATCH 15/38] fw: extract local delivery logic for multicast out of mcast strategy --- fw/fw/bestroute.go | 2 ++ fw/fw/bier_strategy.go | 20 ++------------------ fw/fw/broadcast_strategy.go | 20 +++++--------------- fw/fw/multicast.go | 2 ++ fw/fw/replicast.go | 2 ++ fw/fw/strategy.go | 3 ++- fw/fw/thread.go | 12 +++++++++++- 7 files changed, 26 insertions(+), 35 deletions(-) diff --git a/fw/fw/bestroute.go b/fw/fw/bestroute.go index d3386a97..03c44989 100644 --- a/fw/fw/bestroute.go +++ b/fw/fw/bestroute.go @@ -122,12 +122,14 @@ func (s *BestRoute) AfterReceiveMulticastInterest( pitEntry table.PitEntry, inFace uint64, petEntry table.PetEntry, + deliveredToLocal bool, ) { core.Log.Error(s, "BestRoute does not support AfterReceiveMulticastInterest", "name", packet.Name, "inFace", inFace, "petNextHops", len(petEntry.NextHops), "petEgress", len(petEntry.EgressRouters), + "deliveredToLocal", deliveredToLocal, ) } diff --git a/fw/fw/bier_strategy.go b/fw/fw/bier_strategy.go index 5c6f49dc..f1c59626 100644 --- a/fw/fw/bier_strategy.go +++ b/fw/fw/bier_strategy.go @@ -86,24 +86,8 @@ func (s *BierStrategy) AfterReceiveMulticastInterest( pitEntry table.PitEntry, inFace uint64, petEntry table.PetEntry, + deliveredToLocal bool, ) { - // Deliver to local faces if there exist such legal local faces - deliverToLocal := false - if len(petEntry.NextHops) > 0 { - for i := range petEntry.NextHops { - nexthop := petEntry.NextHops[i] - if nexthop.FaceID == inFace { - continue - } - if pitEntry.InRecords()[nexthop.FaceID] != nil { - continue - } - deliverToLocal = true - packet.EgressRouter = nil - s.SendInterest(packet, pitEntry, nexthop.FaceID, inFace) - } - } - if len(packet.Bier) == 0 { if len(petEntry.EgressRouters) == 0 { core.Log.Trace(s, "Multicast BIER empty without PET egress; drop", "name", packet.Name) @@ -113,7 +97,7 @@ func (s *BierStrategy) AfterReceiveMulticastInterest( packet.Bier = Bift.BuildBierBitString(petEntry.EgressRouters) } - if deliverToLocal && len(packet.Bier) > 0 && IsBierEnabled() { + if deliveredToLocal && len(packet.Bier) > 0 && IsBierEnabled() { bs := BierClone(packet.Bier) BierClearBit(bs, CfgBierIndex()) packet.Bier = bs diff --git a/fw/fw/broadcast_strategy.go b/fw/fw/broadcast_strategy.go index d2fcc11f..e2973279 100644 --- a/fw/fw/broadcast_strategy.go +++ b/fw/fw/broadcast_strategy.go @@ -64,22 +64,12 @@ func (s *BroadcastStrategy) AfterReceiveMulticastInterest( pitEntry table.PitEntry, inFace uint64, petEntry table.PetEntry, + deliveredToLocal bool, ) { - // Deliver to local faces if there exist such legal local faces - if len(petEntry.NextHops) > 0 { - for i := range petEntry.NextHops { - nexthop := petEntry.NextHops[i] - if nexthop.FaceID == inFace { - continue - } - if pitEntry.InRecords()[nexthop.FaceID] != nil { - continue - } - packet.EgressRouter = nil - s.SendInterest(packet, pitEntry, nexthop.FaceID, inFace) - } - } - + core.Log.Trace(s, "Broadcast multicast dispatch", + "name", packet.Name, + "deliveredToLocal", deliveredToLocal, + ) if len(petEntry.EgressRouters) == 0 { core.Log.Trace(s, "Broadcast without PET egress routers; drop", "name", packet.Name) return diff --git a/fw/fw/multicast.go b/fw/fw/multicast.go index cc0147ae..864ad50e 100644 --- a/fw/fw/multicast.go +++ b/fw/fw/multicast.go @@ -95,12 +95,14 @@ func (s *Multicast) AfterReceiveMulticastInterest( pitEntry table.PitEntry, inFace uint64, petEntry table.PetEntry, + deliveredToLocal bool, ) { core.Log.Error(s, "Multicast does not support AfterReceiveMulticastInterest", "name", packet.Name, "inFace", inFace, "petNextHops", len(petEntry.NextHops), "petEgress", len(petEntry.EgressRouters), + "deliveredToLocal", deliveredToLocal, ) } diff --git a/fw/fw/replicast.go b/fw/fw/replicast.go index 7927331d..cd24f067 100644 --- a/fw/fw/replicast.go +++ b/fw/fw/replicast.go @@ -107,12 +107,14 @@ func (s *Replicast) AfterReceiveMulticastInterest( pitEntry table.PitEntry, inFace uint64, petEntry table.PetEntry, + deliveredToLocal bool, ) { core.Log.Error(s, "Replicast does not support AfterReceiveMulticastInterest", "name", packet.Name, "inFace", inFace, "petNextHops", len(petEntry.NextHops), "petEgress", len(petEntry.EgressRouters), + "deliveredToLocal", deliveredToLocal, ) } diff --git a/fw/fw/strategy.go b/fw/fw/strategy.go index 570afe92..1da34330 100644 --- a/fw/fw/strategy.go +++ b/fw/fw/strategy.go @@ -46,7 +46,8 @@ type Strategy interface { packet *defn.Pkt, pitEntry table.PitEntry, inFace uint64, - petEntry table.PetEntry) + petEntry table.PetEntry, + deliveredToLocal bool) BeforeSatisfyInterest( pitEntry table.PitEntry, inFace uint64) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 4cf61e56..e9fd17d5 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -554,8 +554,18 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { "bier", len(packet.Bier), "mcastStrategy", multicastStrategyName, ) + deliveredToLocal := false + if len(petLocalHops) > 0 { + core.Log.Trace(t, "Local Egress captures the Interest", "name", packet.Name) + // In multicast, we deliver to all local faces unlike in unicast + for _, localHop := range petLocalHops { + packet.EgressRouter = nil + t.processOutgoingInterest(packet, pitEntry, localHop.FaceID, incomingFace.FaceID()) + deliveredToLocal = true + } + } strategy := t.strategies[multicastStrategyName.Hash()] - strategy.AfterReceiveMulticastInterest(packet, pitEntry, incomingFace.FaceID(), petEntry) + strategy.AfterReceiveMulticastInterest(packet, pitEntry, incomingFace.FaceID(), petEntry, deliveredToLocal) return } } From 685f287492f935646e971b8faf969380e13afa66 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 31 Mar 2026 00:50:29 -0700 Subject: [PATCH 16/38] fw: delete replicast (doesnt work with 3-pipeline forwarding) + remove overly harsh test restrictions --- dv/config/config.go | 5 -- e2e/dv_util.py | 2 - e2e/test_001.py | 4 +- e2e/test_005.py | 4 +- fw/fw/replicast.go | 124 -------------------------------------------- 5 files changed, 4 insertions(+), 135 deletions(-) delete mode 100644 fw/fw/replicast.go diff --git a/dv/config/config.go b/dv/config/config.go index da983d8a..21cba153 100644 --- a/dv/config/config.go +++ b/dv/config/config.go @@ -32,11 +32,6 @@ var BroadcastStrategy = enc.LOCALHOST. Append(enc.NewGenericComponent("strategy")). Append(enc.NewGenericComponent("broadcast")) -var ReplicastStrategy = enc.LOCALHOST. - Append(enc.NewGenericComponent("nfd")). - Append(enc.NewGenericComponent("strategy")). - Append(enc.NewGenericComponent("replicast")) - //go:embed schema.tlv var SchemaBytes []byte diff --git a/e2e/dv_util.py b/e2e/dv_util.py index 5ce2518e..4c1c4418 100644 --- a/e2e/dv_util.py +++ b/e2e/dv_util.py @@ -59,8 +59,6 @@ def is_converged(nodes: list[Node], network=DEFAULT_NETWORK, use_nfdc=False) -> continue if f'{network}/{other.name}' not in routes: info(f'Routing not converged on {node.name} for {other.name}\n') - preview = "\n".join(routes.splitlines()[:40]) - info(f'route-list({node.name}) first 40 lines:\n{preview}\n') converged = False break # break out of inner loop if not converged: diff --git a/e2e/test_001.py b/e2e/test_001.py index 2f1cbe30..286be799 100644 --- a/e2e/test_001.py +++ b/e2e/test_001.py @@ -18,7 +18,7 @@ def scenario(ndn: Minindn, network='/minindn'): AppManager(ndn, ndn.net.hosts, NDNd_FW, network=network) dv_util.setup(ndn, network=network) - dv_util.converge(ndn.net.hosts, network=network, deadline=20) + dv_util.converge(ndn.net.hosts, network=network) info('Testing file transfer\n') test_file = '/tmp/test.bin' @@ -42,7 +42,7 @@ def scenario(ndn: Minindn, network='/minindn'): # New pipeline requires PET propagation before Interests can be forwarded. expected = {node: set(put_prefixes) for node in ndn.net.hosts} - dv_util.wait_prefix_pet_ready(expected, deadline=20) + dv_util.wait_prefix_pet_ready(expected) # Prefix traffic should remain PET-driven; app prefixes must not be injected into FIB. for node in ndn.net.hosts: diff --git a/e2e/test_005.py b/e2e/test_005.py index de4b280b..00540940 100644 --- a/e2e/test_005.py +++ b/e2e/test_005.py @@ -41,7 +41,7 @@ def scenario(ndn: Minindn, network='/minindn'): AppManager(ndn, [host], NDNd_FW, network=network, bier_index=bier_map[host]) dv_util.setup(ndn, network=network) - dv_util.converge(ndn.net.hosts, network=network, deadline=20) + dv_util.converge(ndn.net.hosts, network=network) dv_util.populate_bift(hosts, bier_map, network=network) producer = sorted(hosts, key=lambda h: h.name)[0] @@ -57,7 +57,7 @@ def scenario(ndn: Minindn, network='/minindn'): # Wait for prefix to appear in PET on every node (confirms BIER egress table is ready) expected = {node: {prefix} for node in hosts} - dv_util.wait_prefix_pet_ready(expected, deadline=20) + dv_util.wait_prefix_pet_ready(expected) info('--- Fetching data via BIER multicast on all consumers ---\n') failures = [] diff --git a/fw/fw/replicast.go b/fw/fw/replicast.go deleted file mode 100644 index cd24f067..00000000 --- a/fw/fw/replicast.go +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Replicast forwarding strategy - * - * Using this strategy, there will be exactly one request sent per unique ER. - * - * This will eventually be replaced by BIER multicast - */ - -package fw - -import ( - "sort" - - "github.com/named-data/ndnd/fw/core" - "github.com/named-data/ndnd/fw/defn" - "github.com/named-data/ndnd/fw/table" -) - -// The strategy to forward interests to all nexthops. -type Replicast struct { - StrategyBase -} - -// (AI GENERATED DESCRIPTION): Registers the Replicast strategy with version 1 in the strategy registry by appending its constructor to the init list. -func init() { - strategyInit = append(strategyInit, func() Strategy { return &Replicast{} }) - StrategyVersions["replicast"] = []uint64{1} -} - -// (AI GENERATED DESCRIPTION): Initializes the *Replicast strategy by setting up its base with the name “replicast” and priority 1 on the provided forwarding thread. -func (s *Replicast) Instantiate(fwThread *Thread) { - s.NewStrategyBase(fwThread, "replicast", 1) -} - -// (AI GENERATED DESCRIPTION): Sends a cached Data packet (retrieved from the Content Store) back to the requester via the specified PIT entry, using the requesting face and indicating the Content Store as the data source. -func (s *Replicast) AfterContentStoreHit( - packet *defn.Pkt, - pitEntry table.PitEntry, - inFace uint64, -) { - core.Log.Trace(s, "AfterContentStoreHit", "name", packet.Name, "faceid", inFace) - s.SendData(packet, pitEntry, inFace, 0) // 0 indicates ContentStore is source -} - -// (AI GENERATED DESCRIPTION): Forwards a received Data packet to every face recorded in the PIT entry, logging each forwarding step and invoking SendData for each destination. -func (s *Replicast) AfterReceiveData( - packet *defn.Pkt, - pitEntry table.PitEntry, - inFace uint64, -) { - core.Log.Trace(s, "AfterReceiveData", "name", packet.Name, "inrecords", len(pitEntry.InRecords())) - for faceID := range pitEntry.InRecords() { - core.Log.Trace(s, "Forwarding Data", "name", packet.Name, "faceid", faceID) - s.SendData(packet, pitEntry, faceID, inFace) - } -} - -// Forwards n interest packets to n unique egress routers -func (s *Replicast) AfterReceiveInterest( - packet *defn.Pkt, - pitEntry table.PitEntry, - inFace uint64, - nexthops []StrategyCandidateHop, -) { - if len(nexthops) == 0 { - core.Log.Debug(s, "No nexthop found - DROP", "name", packet.Name) - return - } - - // interest with ER tag should only be unicast as dest router is known - if len(packet.EgressRouter) > 0 { - core.Log.Debug(s, "Interest with ER tag passed to replicast - DROP", "name", packet.Name) - return - } - - // sort nexthops by cost and send to best-possible nexthop for each unique ER - sort.Slice(nexthops, func(i, j int) bool { - return nexthops[i].HopEntry.Cost < nexthops[j].HopEntry.Cost - }) - - sentER := make(map[uint64]bool) - - for _, nh := range nexthops { - if sentER[nh.EgressRouter.Hash()] { - core.Log.Trace(s, "Avoiding duplicate interest", "name", packet.Name, "sentER", sentER, "faceid", nh.HopEntry.Nexthop, "er", nh.EgressRouter, "inFace", inFace) - continue - } - - outgoingPkt := *packet - outgoingPkt.EgressRouter = nh.EgressRouter - if sent := s.SendInterest(&outgoingPkt, pitEntry, nh.HopEntry.Nexthop, inFace); sent { - core.Log.Trace(s, "Forwarded Interest", "name", outgoingPkt.Name, "faceid", nh.HopEntry.Nexthop, "er", nh.EgressRouter, "inFace", inFace) - sentER[nh.EgressRouter.Hash()] = true - } - } - - for _, nh := range nexthops { - er := nh.EgressRouter - if !sentER[er.Hash()] { - core.Log.Debug(s, "No usable replicast nexthop for Interest specific ER tag - DROP", "name", packet.Name, "ER", er) - } - } -} - -func (s *Replicast) AfterReceiveMulticastInterest( - packet *defn.Pkt, - pitEntry table.PitEntry, - inFace uint64, - petEntry table.PetEntry, - deliveredToLocal bool, -) { - core.Log.Error(s, "Replicast does not support AfterReceiveMulticastInterest", - "name", packet.Name, - "inFace", inFace, - "petNextHops", len(petEntry.NextHops), - "petEgress", len(petEntry.EgressRouters), - "deliveredToLocal", deliveredToLocal, - ) -} - -// (AI GENERATED DESCRIPTION): No‑op hook invoked before satisfying an Interest in the Replicast strategy – it performs no action. -func (s *Replicast) BeforeSatisfyInterest(pitEntry table.PitEntry, inFace uint64) { - // This does nothing in Replicast -} From a7b84969586c8831c8989ba9f3b9a1465ff61e74 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 31 Mar 2026 01:00:42 -0700 Subject: [PATCH 17/38] fw: remove fallback which was not originally present --- fw/fw/thread.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index e9fd17d5..5075541f 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -478,13 +478,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { }) } } - } else { - // Fall back to FIB lookup on the Interest name (or forwarding hint) - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(lookupName) { - nextNet = append(nextNet, StrategyCandidateHop{ - HopEntry: nextHop, - }) - } } if len(nextNet) == 0 { From 667d5176be976dcc6b6e4904ab3621523104100f Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 31 Mar 2026 01:08:28 -0700 Subject: [PATCH 18/38] fw: remove early fibstrategy lookup present for AfterContentStoreHit --- fw/fw/thread.go | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/fw/fw/thread.go b/fw/fw/thread.go index 5075541f..a63d6043 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -333,11 +333,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { "bier", len(packet.Bier) > 0, ) - // Get strategy for name. This may not be the final strategy, - // we need to get a prelim strategy for CS though for AfterContentStoreHit - strategyName := table.FibStrategyTable.FindStrategyEnc(interest.Name()) - strategy := t.strategies[strategyName.Hash()] - // Add in-record and determine if already pending // this looks like custom interest again, but again can be changed without much issue? inRecord, isAlreadyPending, prevNonce := pitEntry.InsertInRecord( @@ -374,17 +369,7 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { packet.L3.Interest = nil packet.Raw = enc.Wire{csWire} packet.Name = csData.NameV - strategy.AfterContentStoreHit(packet, pitEntry, incomingFace.FaceID()) - // TODO - below is equivalent to above AfterContentStoreHit call. replace above with below to not require having a strategy locked in at this stage. - // core.Log.Trace(t, "AfterContentStoreHit", "name", packet.Name, "faceid", incomingFace.FaceID()) - - // nexthop := incomingFace.FaceID() - // var pitToken []byte - // if inRecord, ok := pitEntry.InRecords()[nexthop]; ok { - // pitToken = inRecord.PitToken - // pitEntry.RemoveInRecord(nexthop) - // } - // t.processOutgoingData(packet, nexthop, pitToken, 0) + t.afterContentStoreHit(packet, pitEntry, incomingFace.FaceID()) return } else if err != nil { core.Log.Error(t, "Error copying CS entry", "err", err) @@ -485,7 +470,6 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { "name", packet.Name, "lookup", lookupName, "pipeline", pipeline, - "strategy", strategyName, "isLocalHop", isLocalHop, ) } @@ -563,14 +547,20 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { } } -// processBierInterest is the multicast forwarding pipeline for BIER Interests. -// It is called directly by processInterest (pipeline-level dispatch) rather than -// delegating to a forwarding strategy. This reflects the conceptual distinction -// between unicast and multicast delivery models. -func (t *Thread) processBierInterest(packet *defn.Pkt, pitEntry table.PitEntry, inFace uint64) { - bierReplicate(t, packet, pitEntry, inFace, func(pkt *defn.Pkt, pit table.PitEntry, nexthop, in uint64) bool { - return t.processOutgoingInterest(pkt, pit, nexthop, in) - }) +func (t *Thread) afterContentStoreHit( + packet *defn.Pkt, + pitEntry table.PitEntry, + inFace uint64, +) { + core.Log.Trace(t, "AfterContentStoreHit", "name", packet.Name, "faceid", inFace) + + nexthop := inFace + var pitToken []byte + if inRecord, ok := pitEntry.InRecords()[nexthop]; ok { + pitToken = inRecord.PitToken + pitEntry.RemoveInRecord(nexthop) + } + t.processOutgoingData(packet, nexthop, pitToken, 0) } func (t *Thread) processOutgoingInterest( From 1503b521db8b69c0a70dd87508c1959a37401527 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 31 Mar 2026 01:19:09 -0700 Subject: [PATCH 19/38] e2e: pet prefix ready deadline of 180 for ci --- e2e/test_001.py | 2 +- e2e/test_005.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/test_001.py b/e2e/test_001.py index 286be799..944c2d45 100644 --- a/e2e/test_001.py +++ b/e2e/test_001.py @@ -42,7 +42,7 @@ def scenario(ndn: Minindn, network='/minindn'): # New pipeline requires PET propagation before Interests can be forwarded. expected = {node: set(put_prefixes) for node in ndn.net.hosts} - dv_util.wait_prefix_pet_ready(expected) + dv_util.wait_prefix_pet_ready(expected, deadline=180) # Prefix traffic should remain PET-driven; app prefixes must not be injected into FIB. for node in ndn.net.hosts: diff --git a/e2e/test_005.py b/e2e/test_005.py index 00540940..7e344f2e 100644 --- a/e2e/test_005.py +++ b/e2e/test_005.py @@ -57,7 +57,7 @@ def scenario(ndn: Minindn, network='/minindn'): # Wait for prefix to appear in PET on every node (confirms BIER egress table is ready) expected = {node: {prefix} for node in hosts} - dv_util.wait_prefix_pet_ready(expected) + dv_util.wait_prefix_pet_ready(expected, deadline=180) info('--- Fetching data via BIER multicast on all consumers ---\n') failures = [] From 87fec4de2b276050aa5011c140753f769e9fa7df Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Tue, 31 Mar 2026 01:40:39 -0700 Subject: [PATCH 20/38] fw: filter out forwarding to incoming faceid --- fw/fw/broadcast_strategy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fw/fw/broadcast_strategy.go b/fw/fw/broadcast_strategy.go index e2973279..c5087ec4 100644 --- a/fw/fw/broadcast_strategy.go +++ b/fw/fw/broadcast_strategy.go @@ -74,8 +74,13 @@ func (s *BroadcastStrategy) AfterReceiveMulticastInterest( core.Log.Trace(s, "Broadcast without PET egress routers; drop", "name", packet.Name) return } + for _, er := range petEntry.EgressRouters { for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { + if nextHop.Nexthop == packet.IncomingFaceID { + continue + } + if pitEntry.InRecords()[nextHop.Nexthop] != nil { continue } From f60356ca5397afcaa3553900ecdb22e6ffed7c9e Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Wed, 1 Apr 2026 08:58:02 -0700 Subject: [PATCH 21/38] ci: collect minindn e2e logs on failure --- .github/workflows/test.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b72cf43..1b75a198 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,3 +89,19 @@ jobs: - name: Run e2e tests run: make e2e + + - name: Collect minindn logs on failure + if: failure() + run: | + if [ -d /tmp/minindn ]; then + tar -czf /tmp/minindn-logs.tgz -C /tmp minindn + else + echo "No /tmp/minindn directory found" + fi + + - name: Upload minindn logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: minindn-logs + path: /tmp/minindn-logs.tgz From 723c4cdde0d21c3569ce583c3dbbe6d98929e7ce Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Fri, 3 Apr 2026 00:53:30 -0700 Subject: [PATCH 22/38] fw: broadcast send to all faces in fib instead of by pet er lookup --- fw/fw/broadcast_strategy.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fw/fw/broadcast_strategy.go b/fw/fw/broadcast_strategy.go index c5087ec4..0e2f80dd 100644 --- a/fw/fw/broadcast_strategy.go +++ b/fw/fw/broadcast_strategy.go @@ -70,13 +70,14 @@ func (s *BroadcastStrategy) AfterReceiveMulticastInterest( "name", packet.Name, "deliveredToLocal", deliveredToLocal, ) - if len(petEntry.EgressRouters) == 0 { - core.Log.Trace(s, "Broadcast without PET egress routers; drop", "name", packet.Name) - return - } + seen := make(map[uint64]struct{}) + for _, entry := range table.FibStrategyTable.GetAllFIBEntries() { + for _, nextHop := range entry.GetNextHops() { + if _, ok := seen[nextHop.Nexthop]; ok { + continue + } + seen[nextHop.Nexthop] = struct{}{} - for _, er := range petEntry.EgressRouters { - for _, nextHop := range table.FibStrategyTable.FindNextHopsEnc(er) { if nextHop.Nexthop == packet.IncomingFaceID { continue } From 68b2c38a831143cd3dec29dcd92bece45f95d500 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Fri, 3 Apr 2026 01:47:09 -0700 Subject: [PATCH 23/38] fw: refactor out duplicated multicast-strategy-choice into strategy-choice + remove unneeded strategy name normalization --- fw/mgmt/multicast-strategy-choice.go | 188 --------------------------- fw/mgmt/strategy-choice.go | 67 ++++++++-- fw/mgmt/thread.go | 4 +- 3 files changed, 61 insertions(+), 198 deletions(-) delete mode 100644 fw/mgmt/multicast-strategy-choice.go diff --git a/fw/mgmt/multicast-strategy-choice.go b/fw/mgmt/multicast-strategy-choice.go deleted file mode 100644 index d66ed775..00000000 --- a/fw/mgmt/multicast-strategy-choice.go +++ /dev/null @@ -1,188 +0,0 @@ -/* YaNFD - Yet another NDN Forwarding Daemon - * - * Copyright (C) 2026 - * - * This file is licensed under the terms of the MIT License, as found in LICENSE.md. - */ - -package mgmt - -import ( - "github.com/named-data/ndnd/fw/core" - "github.com/named-data/ndnd/fw/defn" - "github.com/named-data/ndnd/fw/table" - enc "github.com/named-data/ndnd/std/encoding" - mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" -) - -// MulticastStrategyChoiceModule handles multicast strategy choice management. -type MulticastStrategyChoiceModule struct { - manager *Thread -} - -// (AI GENERATED DESCRIPTION): Returns the identifier string "mgmt-multicast-strategy" for the module. -func (s *MulticastStrategyChoiceModule) String() string { - return "mgmt-multicast-strategy" -} - -// (AI GENERATED DESCRIPTION): Registers the specified manager by assigning it to the module's manager field. -func (s *MulticastStrategyChoiceModule) registerManager(manager *Thread) { - s.manager = manager -} - -// (AI GENERATED DESCRIPTION): Returns the manager thread associated with this module. -func (s *MulticastStrategyChoiceModule) getManager() *Thread { - return s.manager -} - -// (AI GENERATED DESCRIPTION): Handles incoming multicast strategy management Interests. -func (s *MulticastStrategyChoiceModule) handleIncomingInterest(interest *Interest) { - // Only allow from /localhost - if !LOCAL_PREFIX.IsPrefix(interest.Name()) { - core.Log.Warn(s, "Received multicast strategy management Interest from non-local source - DROP") - return - } - - verb := interest.Name()[len(LOCAL_PREFIX)+1].String() - switch verb { - case "set": - s.set(interest) - case "unset": - s.unset(interest) - case "list": - s.list(interest) - default: - s.manager.sendCtrlResp(interest, 501, "Unknown verb", nil) - return - } -} - -func normalizeMulticastStrategy(name enc.Name) (enc.Name, bool) { - if !defn.STRATEGY_PREFIX.IsPrefix(name) { - return nil, false - } - - if len(name) == len(defn.STRATEGY_PREFIX)+1 { - strategyName := name[len(defn.STRATEGY_PREFIX)].String() - switch strategyName { - case "broadcast": - return defn.BROADCAST_STRATEGY, true - case "bier": - return defn.BIER_STRATEGY, true - default: - return nil, false - } - } - - if len(name) == len(defn.STRATEGY_PREFIX)+2 && name[len(defn.STRATEGY_PREFIX)+1].IsVersion() { - strategyName := name[len(defn.STRATEGY_PREFIX)].String() - versionBytes := name[len(defn.STRATEGY_PREFIX)+1].Val - version, _, err := enc.ParseNat(versionBytes) - if err != nil || version != 1 { - return nil, false - } - switch strategyName { - case "broadcast": - return defn.BROADCAST_STRATEGY, true - case "bier": - return defn.BIER_STRATEGY, true - default: - return nil, false - } - } - - return nil, false -} - -// (AI GENERATED DESCRIPTION): Handles a SetStrategy control request for multicast strategies. -func (s *MulticastStrategyChoiceModule) set(interest *Interest) { - if len(interest.Name()) < len(LOCAL_PREFIX)+3 { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) - return - } - - params := decodeControlParameters(s, interest) - if params == nil { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) - return - } - - if params.Name == nil { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (missing Name)", nil) - return - } - - if params.Strategy == nil { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (missing Strategy)", nil) - return - } - - normalized, ok := normalizeMulticastStrategy(params.Strategy.Name) - if !ok { - core.Log.Warn(s, "Invalid multicast strategy", "strategy", params.Strategy.Name) - s.manager.sendCtrlResp(interest, 404, "Invalid multicast strategy", nil) - return - } - - table.MulticastStrategyTable.SetStrategyEnc(params.Name, normalized) - - s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{ - Name: params.Name, - Strategy: &mgmt.Strategy{Name: normalized}, - }) - - core.Log.Info(s, "Set multicast strategy", "name", params.Name, "strategy", normalized) -} - -// (AI GENERATED DESCRIPTION): Unsets a multicast strategy for a given name. -func (s *MulticastStrategyChoiceModule) unset(interest *Interest) { - if len(interest.Name()) < len(LOCAL_PREFIX)+3 { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) - return - } - - params := decodeControlParameters(s, interest) - if params == nil { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect", nil) - return - } - - if params.Name == nil { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (missing Name)", nil) - return - } - - if len(params.Name) == 0 { - s.manager.sendCtrlResp(interest, 400, "ControlParameters is incorrect (empty Name)", nil) - return - } - - table.MulticastStrategyTable.UnSetStrategyEnc(params.Name) - core.Log.Info(s, "Unset multicast strategy", "name", params.Name) - - s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{Name: params.Name}) -} - -// (AI GENERATED DESCRIPTION): Handles listing multicast strategies. -func (s *MulticastStrategyChoiceModule) list(interest *Interest) { - if len(interest.Name()) > len(LOCAL_PREFIX)+2 { - // Ignore because contains version and/or segment components - return - } - - entries := table.MulticastStrategyTable.GetAllForwardingStrategies() - choices := []*mgmt.StrategyChoice{} - for _, fsEntry := range entries { - choices = append(choices, &mgmt.StrategyChoice{ - Name: fsEntry.Name(), - Strategy: &mgmt.Strategy{Name: fsEntry.GetStrategy()}, - }) - } - dataset := &mgmt.StrategyChoiceMsg{StrategyChoices: choices} - - name := LOCAL_PREFIX.Append( - enc.NewGenericComponent("multicast-strategy-choice"), - enc.NewGenericComponent("list"), - ) - s.manager.sendStatusDataset(interest, name, dataset.Encode()) -} diff --git a/fw/mgmt/strategy-choice.go b/fw/mgmt/strategy-choice.go index 492a6026..b63f6458 100644 --- a/fw/mgmt/strategy-choice.go +++ b/fw/mgmt/strategy-choice.go @@ -19,10 +19,25 @@ import ( // StrategyChoiceModule is the module that handles Strategy Choice Management. type StrategyChoiceModule struct { manager *Thread + kind strategyChoiceKind +} + +type strategyChoiceKind int + +const ( + strategyChoiceUnicast strategyChoiceKind = iota + strategyChoiceMulticast +) + +func NewStrategyChoiceModule(kind strategyChoiceKind) *StrategyChoiceModule { + return &StrategyChoiceModule{kind: kind} } // (AI GENERATED DESCRIPTION): Returns the identifier string `"mgmt-strategy"` for the StrategyChoiceModule. func (s *StrategyChoiceModule) String() string { + if s.kind == strategyChoiceMulticast { + return "mgmt-multicast-strategy" + } return "mgmt-strategy" } @@ -40,7 +55,11 @@ func (s *StrategyChoiceModule) getManager() *Thread { func (s *StrategyChoiceModule) handleIncomingInterest(interest *Interest) { // Only allow from /localhost if !LOCAL_PREFIX.IsPrefix(interest.Name()) { - core.Log.Warn(s, "Received strategy management Interest from non-local source - DROP") + if s.kind == strategyChoiceMulticast { + core.Log.Warn(s, "Received multicast strategy management Interest from non-local source - DROP") + } else { + core.Log.Warn(s, "Received strategy management Interest from non-local source - DROP") + } return } @@ -82,6 +101,24 @@ func (s *StrategyChoiceModule) set(interest *Interest) { return } + if s.kind == strategyChoiceMulticast { + if !params.Strategy.Name.Equal(defn.BROADCAST_STRATEGY) && !params.Strategy.Name.Equal(defn.BIER_STRATEGY) { + core.Log.Warn(s, "Invalid multicast strategy", "strategy", params.Strategy.Name) + s.manager.sendCtrlResp(interest, 404, "Invalid multicast strategy", nil) + return + } + + table.MulticastStrategyTable.SetStrategyEnc(params.Name, params.Strategy.Name) + + s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{ + Name: params.Name, + Strategy: params.Strategy, + }) + + core.Log.Info(s, "Set multicast strategy", "name", params.Name, "strategy", params.Strategy.Name) + return + } + if !defn.STRATEGY_PREFIX.IsPrefix(params.Strategy.Name) { core.Log.Warn(s, "Invalid strategy", "strategy", params.Strategy.Name) s.manager.sendCtrlResp(interest, 404, "Invalid strategy", nil) @@ -164,8 +201,13 @@ func (s *StrategyChoiceModule) unset(interest *Interest) { return } - table.FibStrategyTable.UnSetStrategyEnc(params.Name) - core.Log.Info(s, "Unset Strategy", "name", params.Name) + if s.kind == strategyChoiceMulticast { + table.MulticastStrategyTable.UnSetStrategyEnc(params.Name) + core.Log.Info(s, "Unset multicast strategy", "name", params.Name) + } else { + table.FibStrategyTable.UnSetStrategyEnc(params.Name) + core.Log.Info(s, "Unset Strategy", "name", params.Name) + } s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{Name: params.Name}) } @@ -179,7 +221,20 @@ func (s *StrategyChoiceModule) list(interest *Interest) { // Generate new dataset // TODO: For thread safety, we should lock the Strategy table from writes until we are done - entries := table.FibStrategyTable.GetAllForwardingStrategies() + var entries []table.FibStrategyEntry + name := LOCAL_PREFIX.Append( + enc.NewGenericComponent("strategy-choice"), + enc.NewGenericComponent("list"), + ) + if s.kind == strategyChoiceMulticast { + entries = table.MulticastStrategyTable.GetAllForwardingStrategies() + name = LOCAL_PREFIX.Append( + enc.NewGenericComponent("multicast-strategy-choice"), + enc.NewGenericComponent("list"), + ) + } else { + entries = table.FibStrategyTable.GetAllForwardingStrategies() + } choices := []*mgmt.StrategyChoice{} for _, fsEntry := range entries { choices = append(choices, &mgmt.StrategyChoice{ @@ -189,9 +244,5 @@ func (s *StrategyChoiceModule) list(interest *Interest) { } dataset := &mgmt.StrategyChoiceMsg{StrategyChoices: choices} - name := LOCAL_PREFIX.Append( - enc.NewGenericComponent("strategy-choice"), - enc.NewGenericComponent("list"), - ) s.manager.sendStatusDataset(interest, name, dataset.Encode()) } diff --git a/fw/mgmt/thread.go b/fw/mgmt/thread.go index 6b25b209..6a444929 100644 --- a/fw/mgmt/thread.go +++ b/fw/mgmt/thread.go @@ -63,8 +63,8 @@ func MakeMgmtThread() *Thread { m.registerModule("bift", newBiftModule()) m.registerModule("rib", new(RIBModule)) m.registerModule("status", new(ForwarderStatusModule)) - m.registerModule("strategy-choice", new(StrategyChoiceModule)) - m.registerModule("multicast-strategy-choice", new(MulticastStrategyChoiceModule)) + m.registerModule("strategy-choice", NewStrategyChoiceModule(strategyChoiceUnicast)) + m.registerModule("multicast-strategy-choice", NewStrategyChoiceModule(strategyChoiceMulticast)) // readvertisers run in the management thread for ease of // implementation, since they use the internal transport From 4541b7a82a41e79c716b752b6b72adcd727c7689 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Fri, 3 Apr 2026 15:45:37 -0700 Subject: [PATCH 24/38] dv: get rid of retries param in helper fn --- dv/dv/router.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dv/dv/router.go b/dv/dv/router.go index ea8fb9ba..7ae82390 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -288,7 +288,7 @@ func (dv *Router) register() (err error) { for _, prefix := range pfxs { dv.execMgmtRetry("pet", "add-nexthop", &mgmt.ControlArgs{ Name: prefix, - }, -1) + }) } // Allow outgoing local-prefix-sync Interests to use two-phase forwarding. // Incoming Interests still terminate locally on the same prefix. @@ -297,13 +297,13 @@ func (dv *Router) register() (err error) { Name: dv.pfx.SyncPrefix(), Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, Flags: optional.Some(uint64(1)), - }, -1) + }) // Set Advertisement Sync to localhop neighbors dv.execMgmtRetry("pet", "add-egress", &mgmt.ControlArgs{ Name: dv.config.AdvertisementSyncPrefix(), Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, Flags: optional.Some(uint64(1)), - }, -1) + }) // Force multicast strategy for sync prefixes to broadcast. broadcastPrefixes := []enc.Name{ @@ -314,14 +314,14 @@ func (dv *Router) register() (err error) { dv.execMgmtRetry("multicast-strategy-choice", "set", &mgmt.ControlArgs{ Name: prefix, Strategy: &mgmt.Strategy{Name: defn.BROADCAST_STRATEGY}, - }, -1) + }) } return nil } -func (dv *Router) execMgmtRetry(module, cmd string, args *mgmt.ControlArgs, retries int) { - for i := 0; i < retries || retries < 0; i++ { +func (dv *Router) execMgmtRetry(module, cmd string, args *mgmt.ControlArgs) { + for i := 0; ; i++ { if _, err := dv.engine.ExecMgmtCmd(module, cmd, args); err != nil { log.Error(dv, "Forwarder command failed", "err", err, "attempt", i, "module", module, "cmd", cmd, "args", args) From 8bdae7ecd779525eea13f842c47230409ac8b00b Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Fri, 3 Apr 2026 16:02:06 -0700 Subject: [PATCH 25/38] dv: explicit multicast flag in pet mgmt cmd --- dv/dv/mgmt.go | 2 +- dv/dv/prefix.go | 3 +-- dv/dv/router.go | 13 ++++++------ fw/mgmt/pet.go | 3 +-- std/ndn/mgmt_2022/definitions.go | 2 ++ std/ndn/mgmt_2022/zz_generated.go | 34 +++++++++++++++++++++++++++++++ 6 files changed, 45 insertions(+), 12 deletions(-) diff --git a/dv/dv/mgmt.go b/dv/dv/mgmt.go index 7eb2b9a0..b16b1b84 100644 --- a/dv/dv/mgmt.go +++ b/dv/dv/mgmt.go @@ -292,7 +292,7 @@ func (dv *Router) mgmtOnPrefix(args ndn.InterestHandlerArgs) { responseParams.ExpirationPeriod = optional.Some(expires) } - multicast := params.Val.Flags.GetOr(0)&1 != 0 + multicast := params.Val.Multicast dv.mutex.Lock() dv.pfx.Announce(name, faceID, cost, multicast, validity) dv.mutex.Unlock() diff --git a/dv/dv/prefix.go b/dv/dv/prefix.go index f4bcede0..d0b38c94 100644 --- a/dv/dv/prefix.go +++ b/dv/dv/prefix.go @@ -425,9 +425,8 @@ func (pfx *PrefixModule) applyPetOps(ops []petEgressOp) { } if op.add { cmd = "add-egress" - // Signal the Sync group (multicast) flag to the forwarder's PET via Flags bit 0. if op.multicast { - args.Flags = optional.Some(uint64(1)) + args.Multicast = true } } diff --git a/dv/dv/router.go b/dv/dv/router.go index 7ae82390..bc561b15 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -292,17 +292,16 @@ func (dv *Router) register() (err error) { } // Allow outgoing local-prefix-sync Interests to use two-phase forwarding. // Incoming Interests still terminate locally on the same prefix. - // Add multicast flag to the PET announcement dv.execMgmtRetry("pet", "add-egress", &mgmt.ControlArgs{ - Name: dv.pfx.SyncPrefix(), - Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, - Flags: optional.Some(uint64(1)), + Name: dv.pfx.SyncPrefix(), + Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, + Multicast: true, }) // Set Advertisement Sync to localhop neighbors dv.execMgmtRetry("pet", "add-egress", &mgmt.ControlArgs{ - Name: dv.config.AdvertisementSyncPrefix(), - Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, - Flags: optional.Some(uint64(1)), + Name: dv.config.AdvertisementSyncPrefix(), + Egress: &mgmt.EgressRecord{Name: neighborsPrefix.Clone()}, + Multicast: true, }) // Force multicast strategy for sync prefixes to broadcast. diff --git a/fw/mgmt/pet.go b/fw/mgmt/pet.go index d9533907..d646785c 100644 --- a/fw/mgmt/pet.go +++ b/fw/mgmt/pet.go @@ -84,8 +84,7 @@ func (p *PETModule) addEgress(interest *Interest) { return } - // Flags bit 0 signals a Sync group (multicast) prefix announcement from ndn-dv. - multicast := params.Flags.GetOr(0)&1 != 0 + multicast := params.Multicast table.Pet.AddEgressEnc(params.Name, params.Egress.Name, multicast) core.Log.Info(p, "Added PET egress", "name", params.Name, "egress", params.Egress.Name, "multicast", multicast) diff --git a/std/ndn/mgmt_2022/definitions.go b/std/ndn/mgmt_2022/definitions.go index fd7b71e9..c39efbbe 100644 --- a/std/ndn/mgmt_2022/definitions.go +++ b/std/ndn/mgmt_2022/definitions.go @@ -77,6 +77,8 @@ type ControlArgs struct { Flags optional.Optional[uint64] `tlv:"0x6c"` //+field:natural:optional Mask optional.Optional[uint64] `tlv:"0x70"` + //+field:bool + Multicast bool `tlv:"0x90"` //+field:struct:Strategy Strategy *Strategy `tlv:"0x6b"` //+field:natural:optional diff --git a/std/ndn/mgmt_2022/zz_generated.go b/std/ndn/mgmt_2022/zz_generated.go index 28e6626c..823607d4 100644 --- a/std/ndn/mgmt_2022/zz_generated.go +++ b/std/ndn/mgmt_2022/zz_generated.go @@ -251,6 +251,10 @@ func (encoder *ControlArgsEncoder) Init(value *ControlArgs) { l += 1 l += uint(1 + enc.Nat(optval).EncodingLength()) } + if value.Multicast { + l += 1 + l += 1 + } if value.Strategy != nil { l += 1 l += uint(enc.TLNum(encoder.Strategy_encoder.Length).EncodingLength()) @@ -379,6 +383,12 @@ func (encoder *ControlArgsEncoder) EncodeInto(value *ControlArgs, buf []byte) { pos += uint(1 + buf[pos]) } + if value.Multicast { + buf[pos] = byte(144) + pos += 1 + buf[pos] = byte(0) + pos += 1 + } if value.Strategy != nil { buf[pos] = byte(107) pos += 1 @@ -453,6 +463,7 @@ func (context *ControlArgsParsingContext) Parse(reader enc.WireView, ignoreCriti var handled_Count bool = false var handled_Flags bool = false var handled_Mask bool = false + var handled_Multicast bool = false var handled_Strategy bool = false var handled_ExpirationPeriod bool = false var handled_FacePersistency bool = false @@ -683,6 +694,13 @@ func (context *ControlArgsParsingContext) Parse(reader enc.WireView, ignoreCriti value.Mask.Set(optval) } } + case 144: + if true { + handled = true + handled_Multicast = true + value.Multicast = true + err = reader.Skip(int(l)) + } case 107: if true { handled = true @@ -855,6 +873,9 @@ func (context *ControlArgsParsingContext) Parse(reader enc.WireView, ignoreCriti if !handled_Mask && err == nil { value.Mask.Unset() } + if !handled_Multicast && err == nil { + value.Multicast = false + } if !handled_Strategy && err == nil { value.Strategy = nil } @@ -932,6 +953,7 @@ func (value *ControlArgs) ToDict() map[string]any { if optval, ok := value.Mask.Get(); ok { dict["Mask"] = optval } + dict["Multicast"] = value.Multicast if value.Strategy != nil { dict["Strategy"] = value.Strategy.ToDict() } @@ -1088,6 +1110,18 @@ func DictToControlArgs(dict map[string]any) (*ControlArgs, error) { if err != nil { return nil, err } + if vv, ok := dict["Multicast"]; ok { + if v, ok := vv.(bool); ok { + value.Multicast = v + } else { + err = enc.ErrIncompatibleType{Name: "Multicast", TypeNum: 144, ValType: "bool", Value: vv} + } + } else { + value.Multicast = false + } + if err != nil { + return nil, err + } if vv, ok := dict["Strategy"]; ok { if v, ok := vv.(*Strategy); ok { value.Strategy = v From 7e8a30da3caa3c1eceb8acf43e31033ba95a5a77 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Sun, 5 Apr 2026 17:41:41 -0700 Subject: [PATCH 26/38] std: use new multicast arg in client announce --- std/object/client_announce.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/object/client_announce.go b/std/object/client_announce.go index 9d0660ff..6433347a 100644 --- a/std/object/client_announce.go +++ b/std/object/client_announce.go @@ -52,7 +52,7 @@ func (c *Client) announcePrefix_(args ndn.Announcement) { Cost: optional.Some(args.Cost), } if args.Multicast { - ctrlArgs.Flags = optional.Some(uint64(1)) + ctrlArgs.Multicast = true } _, err := mgmt.ExecServiceCmd( c.engine, true, "dv", "prefix", "announce", From 8ecfdab495bb6fe169aba853b1405ab0b44a4bb6 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Sun, 5 Apr 2026 17:46:07 -0700 Subject: [PATCH 27/38] fw: remove bier empty sanity check --- fw/fw/bier_strategy.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fw/fw/bier_strategy.go b/fw/fw/bier_strategy.go index f1c59626..29e96dbd 100644 --- a/fw/fw/bier_strategy.go +++ b/fw/fw/bier_strategy.go @@ -89,10 +89,6 @@ func (s *BierStrategy) AfterReceiveMulticastInterest( deliveredToLocal bool, ) { if len(packet.Bier) == 0 { - if len(petEntry.EgressRouters) == 0 { - core.Log.Trace(s, "Multicast BIER empty without PET egress; drop", "name", packet.Name) - return - } core.Log.Trace(s, "BFIR: encoding BIER bit-string", "name", packet.Name, "egress-count", len(petEntry.EgressRouters)) packet.Bier = Bift.BuildBierBitString(petEntry.EgressRouters) } From 3786b718b17c9054f00c33a273b3b95e3ef93494 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Sun, 5 Apr 2026 21:00:25 -0700 Subject: [PATCH 28/38] fw: name broadcast file --- fw/fw/{broadcast_strategy.go => broadcast.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fw/fw/{broadcast_strategy.go => broadcast.go} (100%) diff --git a/fw/fw/broadcast_strategy.go b/fw/fw/broadcast.go similarity index 100% rename from fw/fw/broadcast_strategy.go rename to fw/fw/broadcast.go From 2d9774a91160088a7e4c4e58abbc8159693a5e40 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Sun, 5 Apr 2026 21:07:10 -0700 Subject: [PATCH 29/38] dv: simplify retry loop --- dv/dv/router.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dv/dv/router.go b/dv/dv/router.go index bc561b15..087bad84 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -320,15 +320,14 @@ func (dv *Router) register() (err error) { } func (dv *Router) execMgmtRetry(module, cmd string, args *mgmt.ControlArgs) { + var err error for i := 0; ; i++ { - if _, err := dv.engine.ExecMgmtCmd(module, cmd, args); err != nil { - log.Error(dv, "Forwarder command failed", "err", err, "attempt", i, - "module", module, "cmd", cmd, "args", args) - time.Sleep(100 * time.Millisecond) - continue + if _, err = dv.engine.ExecMgmtCmd(module, cmd, args); err == nil { + break } - time.Sleep(1 * time.Millisecond) - break + log.Error(dv, "Forwarder command failed", "err", err, "attempt", i, + "module", module, "cmd", cmd, "args", args) + time.Sleep(100 * time.Millisecond) } } From 11ed32f640735d04cd27bd091ae8d67559f9736d Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Sun, 5 Apr 2026 21:12:13 -0700 Subject: [PATCH 30/38] fw: switch on strategy choice kind --- fw/mgmt/strategy-choice.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/fw/mgmt/strategy-choice.go b/fw/mgmt/strategy-choice.go index b63f6458..ceaf91df 100644 --- a/fw/mgmt/strategy-choice.go +++ b/fw/mgmt/strategy-choice.go @@ -101,7 +101,8 @@ func (s *StrategyChoiceModule) set(interest *Interest) { return } - if s.kind == strategyChoiceMulticast { + switch s.kind { + case strategyChoiceMulticast: if !params.Strategy.Name.Equal(defn.BROADCAST_STRATEGY) && !params.Strategy.Name.Equal(defn.BIER_STRATEGY) { core.Log.Warn(s, "Invalid multicast strategy", "strategy", params.Strategy.Name) s.manager.sendCtrlResp(interest, 404, "Invalid multicast strategy", nil) @@ -117,6 +118,12 @@ func (s *StrategyChoiceModule) set(interest *Interest) { core.Log.Info(s, "Set multicast strategy", "name", params.Name, "strategy", params.Strategy.Name) return + case strategyChoiceUnicast: + // fallthrough to unicast handling below + default: + core.Log.Warn(s, "Unknown strategy choice kind", "kind", s.kind) + s.manager.sendCtrlResp(interest, 500, "Internal error", nil) + return } if !defn.STRATEGY_PREFIX.IsPrefix(params.Strategy.Name) { @@ -201,12 +208,17 @@ func (s *StrategyChoiceModule) unset(interest *Interest) { return } - if s.kind == strategyChoiceMulticast { + switch s.kind { + case strategyChoiceMulticast: table.MulticastStrategyTable.UnSetStrategyEnc(params.Name) core.Log.Info(s, "Unset multicast strategy", "name", params.Name) - } else { + case strategyChoiceUnicast: table.FibStrategyTable.UnSetStrategyEnc(params.Name) core.Log.Info(s, "Unset Strategy", "name", params.Name) + default: + core.Log.Warn(s, "Unknown strategy choice kind", "kind", s.kind) + s.manager.sendCtrlResp(interest, 500, "Internal error", nil) + return } s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{Name: params.Name}) @@ -226,14 +238,18 @@ func (s *StrategyChoiceModule) list(interest *Interest) { enc.NewGenericComponent("strategy-choice"), enc.NewGenericComponent("list"), ) - if s.kind == strategyChoiceMulticast { + switch s.kind { + case strategyChoiceMulticast: entries = table.MulticastStrategyTable.GetAllForwardingStrategies() name = LOCAL_PREFIX.Append( enc.NewGenericComponent("multicast-strategy-choice"), enc.NewGenericComponent("list"), ) - } else { + case strategyChoiceUnicast: entries = table.FibStrategyTable.GetAllForwardingStrategies() + default: + core.Log.Warn(s, "Unknown strategy choice kind", "kind", s.kind) + return } choices := []*mgmt.StrategyChoice{} for _, fsEntry := range entries { From 8f60368d5daae45bffe85f87489148c2f843ff4f Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Sun, 5 Apr 2026 23:16:08 -0700 Subject: [PATCH 31/38] fw: incorporate strategy verification logic for multicast --- fw/mgmt/strategy-choice.go | 43 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/fw/mgmt/strategy-choice.go b/fw/mgmt/strategy-choice.go index ceaf91df..0003a461 100644 --- a/fw/mgmt/strategy-choice.go +++ b/fw/mgmt/strategy-choice.go @@ -101,31 +101,6 @@ func (s *StrategyChoiceModule) set(interest *Interest) { return } - switch s.kind { - case strategyChoiceMulticast: - if !params.Strategy.Name.Equal(defn.BROADCAST_STRATEGY) && !params.Strategy.Name.Equal(defn.BIER_STRATEGY) { - core.Log.Warn(s, "Invalid multicast strategy", "strategy", params.Strategy.Name) - s.manager.sendCtrlResp(interest, 404, "Invalid multicast strategy", nil) - return - } - - table.MulticastStrategyTable.SetStrategyEnc(params.Name, params.Strategy.Name) - - s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{ - Name: params.Name, - Strategy: params.Strategy, - }) - - core.Log.Info(s, "Set multicast strategy", "name", params.Name, "strategy", params.Strategy.Name) - return - case strategyChoiceUnicast: - // fallthrough to unicast handling below - default: - core.Log.Warn(s, "Unknown strategy choice kind", "kind", s.kind) - s.manager.sendCtrlResp(interest, 500, "Internal error", nil) - return - } - if !defn.STRATEGY_PREFIX.IsPrefix(params.Strategy.Name) { core.Log.Warn(s, "Invalid strategy", "strategy", params.Strategy.Name) s.manager.sendCtrlResp(interest, 404, "Invalid strategy", nil) @@ -175,14 +150,26 @@ func (s *StrategyChoiceModule) set(interest *Interest) { params.Strategy.Name = params.Strategy.Name. Append(enc.NewVersionComponent(strategyVersion)) } - table.FibStrategyTable.SetStrategyEnc(params.Name, params.Strategy.Name) + + switch s.kind { + case strategyChoiceMulticast: + table.MulticastStrategyTable.SetStrategyEnc(params.Name, params.Strategy.Name) + core.Log.Info(s, "Set multicast strategy", "name", params.Name, "strategy", params.Strategy.Name) + + case strategyChoiceUnicast: + table.FibStrategyTable.SetStrategyEnc(params.Name, params.Strategy.Name) + core.Log.Info(s, "Set strategy", "name", params.Name, "strategy", params.Strategy.Name) + + default: + core.Log.Warn(s, "Unknown strategy choice kind", "kind", s.kind) + s.manager.sendCtrlResp(interest, 500, "Internal error", nil) + return + } s.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{ Name: params.Name, Strategy: params.Strategy, }) - - core.Log.Info(s, "Set strategy", "name", params.Name, "strategy", params.Strategy.Name) } // (AI GENERATED DESCRIPTION): Unsets a strategy encoding for a given name by handling a control interest, validating its parameters, removing the strategy from the FIB strategy table, and replying with a 200 OK response. From f90c2f4903080a849ee3ea810e552439f0bd472a Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 6 Apr 2026 01:28:12 -0700 Subject: [PATCH 32/38] fw: switch in strategy choice String --- fw/mgmt/strategy-choice.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/fw/mgmt/strategy-choice.go b/fw/mgmt/strategy-choice.go index 0003a461..9b68d7ad 100644 --- a/fw/mgmt/strategy-choice.go +++ b/fw/mgmt/strategy-choice.go @@ -35,10 +35,12 @@ func NewStrategyChoiceModule(kind strategyChoiceKind) *StrategyChoiceModule { // (AI GENERATED DESCRIPTION): Returns the identifier string `"mgmt-strategy"` for the StrategyChoiceModule. func (s *StrategyChoiceModule) String() string { - if s.kind == strategyChoiceMulticast { + switch s.kind { + case strategyChoiceMulticast: return "mgmt-multicast-strategy" + default: + return "mgmt-strategy" } - return "mgmt-strategy" } // (AI GENERATED DESCRIPTION): Registers the specified manager by assigning it to the StrategyChoiceModule’s manager field. @@ -55,11 +57,7 @@ func (s *StrategyChoiceModule) getManager() *Thread { func (s *StrategyChoiceModule) handleIncomingInterest(interest *Interest) { // Only allow from /localhost if !LOCAL_PREFIX.IsPrefix(interest.Name()) { - if s.kind == strategyChoiceMulticast { - core.Log.Warn(s, "Received multicast strategy management Interest from non-local source - DROP") - } else { - core.Log.Warn(s, "Received strategy management Interest from non-local source - DROP") - } + core.Log.Warn(s, "Received strategy management Interest from non-local source - DROP") return } From 7d152259579e82a6081b24665241e47c77e616e1 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 6 Apr 2026 09:59:40 -0700 Subject: [PATCH 33/38] fw: move bier into contained package --- dv/dv/table_algo.go | 6 +- fw/{fw => bier}/bier.go | 2 +- .../bier_conceptual_test.go | 42 +++--- .../bier_fib_rebuild_test.go | 14 +- .../bier_integration_test.go | 122 +++++++++--------- fw/{bier_tests => bier}/bier_test.go | 106 +++++++-------- fw/{bier_tests => bier}/bier_topo_test.go | 34 ++--- fw/fw/bier_strategy.go | 33 ++--- fw/fw/thread.go | 5 +- fw/mgmt/bift.go | 6 +- 10 files changed, 186 insertions(+), 184 deletions(-) rename fw/{fw => bier}/bier.go (99%) rename fw/{bier_tests => bier}/bier_conceptual_test.go (96%) rename fw/{bier_tests => bier}/bier_fib_rebuild_test.go (97%) rename fw/{bier_tests => bier}/bier_integration_test.go (79%) rename fw/{bier_tests => bier}/bier_test.go (77%) rename fw/{bier_tests => bier}/bier_topo_test.go (98%) diff --git a/dv/dv/table_algo.go b/dv/dv/table_algo.go index 029efb81..c84ea5dc 100644 --- a/dv/dv/table_algo.go +++ b/dv/dv/table_algo.go @@ -3,7 +3,7 @@ package dv import ( "github.com/named-data/ndnd/dv/config" "github.com/named-data/ndnd/dv/table" - fw "github.com/named-data/ndnd/fw/fw" + "github.com/named-data/ndnd/fw/bier" enc "github.com/named-data/ndnd/std/encoding" "github.com/named-data/ndnd/std/log" ) @@ -138,7 +138,7 @@ func (dv *Router) updateFib() { // Rebuild the BIFT whenever the FIB changes so BIER forwarding paths // are always consistent with the routing table. - if fw.IsBierEnabled() { - fw.Bift.BuildFromFib() + if bier.IsBierEnabled() { + bier.Bift.BuildFromFib() } } diff --git a/fw/fw/bier.go b/fw/bier/bier.go similarity index 99% rename from fw/fw/bier.go rename to fw/bier/bier.go index 4ab8cf90..460ca533 100644 --- a/fw/fw/bier.go +++ b/fw/bier/bier.go @@ -5,7 +5,7 @@ * The BIFT maps each bit position to a next-hop face and forwarding bit mask. */ -package fw +package bier import ( "sort" diff --git a/fw/bier_tests/bier_conceptual_test.go b/fw/bier/bier_conceptual_test.go similarity index 96% rename from fw/bier_tests/bier_conceptual_test.go rename to fw/bier/bier_conceptual_test.go index db3e1f60..d3eaa3e1 100644 --- a/fw/bier_tests/bier_conceptual_test.go +++ b/fw/bier/bier_conceptual_test.go @@ -1,4 +1,4 @@ -package bier_tests +package bier_test // Tests for BIER forwarding in NDN: Sync Interest multicast delivery, // prefix-to-routing mapping, BFIR encoding, and BFR/BFER per-hop handling. @@ -7,7 +7,7 @@ import ( "fmt" "testing" - fw "github.com/named-data/ndnd/fw/fw" + bier "github.com/named-data/ndnd/fw/bier" enc "github.com/named-data/ndnd/std/encoding" ) @@ -76,7 +76,7 @@ func TestSyncInterestStatelessForwarding(t *testing.T) { t.Run("Same Sync Interest sent twice produces identical results", func(t *testing.T) { bs := g.buildBitstring(1, 3, 4) res1 := g.simulate(0, bs) - res2 := g.simulate(0, fw.BierClone(bs)) + res2 := g.simulate(0, bier.BierClone(bs)) // Both runs must deliver to exactly the same set for id := range res1.delivered { @@ -98,7 +98,7 @@ func TestPrefixToRoutingMapping(t *testing.T) { // list that BuildBierBitString transforms into a BIER bit-string. t.Run("Group members map to correct bit positions", func(t *testing.T) { - bift := &fw.BiftState{} + bift := &bier.BiftState{} // Mapping protocol populates router-to-BFR-ID assignments // (In production this comes from DV advertisements) @@ -119,22 +119,22 @@ func TestPrefixToRoutingMapping(t *testing.T) { bs := bift.BuildBierBitString(egressRouters) // Verify: bits 0, 2, 3 set (routerA, routerC, routerD) - if !fw.BierGetBit(bs, 0) { + if !bier.BierGetBit(bs, 0) { t.Error("routerA (bit 0) should be set") } - if fw.BierGetBit(bs, 1) { + if bier.BierGetBit(bs, 1) { t.Error("routerB (bit 1) should NOT be set — not in egress list") } - if !fw.BierGetBit(bs, 2) { + if !bier.BierGetBit(bs, 2) { t.Error("routerC (bit 2) should be set") } - if !fw.BierGetBit(bs, 3) { + if !bier.BierGetBit(bs, 3) { t.Error("routerD (bit 3) should be set") } }) t.Run("Unknown routers in egress list are safely ignored", func(t *testing.T) { - bift := &fw.BiftState{} + bift := &bier.BiftState{} known := enc.Name{enc.NewGenericComponent("known-router")} unknown := enc.Name{enc.NewGenericComponent("unknown-router")} @@ -146,24 +146,24 @@ func TestPrefixToRoutingMapping(t *testing.T) { bs := bift.BuildBierBitString(egressRouters) // Only the known router should appear in the bit-string - if !fw.BierGetBit(bs, 5) { + if !bier.BierGetBit(bs, 5) { t.Error("known router (bit 5) should be set") } // Bit-string should be minimal — no stray bits for i := 0; i < 5; i++ { - if fw.BierGetBit(bs, i) { + if bier.BierGetBit(bs, i) { t.Errorf("bit %d should not be set", i) } } }) t.Run("Single egress router produces single-bit string", func(t *testing.T) { - bift := &fw.BiftState{} + bift := &bier.BiftState{} r := enc.Name{enc.NewGenericComponent("solo")} bift.RegisterRouter(r, 7) bs := bift.BuildBierBitString([]enc.Name{r}) - if !fw.BierGetBit(bs, 7) { + if !bier.BierGetBit(bs, 7) { t.Error("single-router bit-string should have bit 7 set") } // All other bits must be clear @@ -183,7 +183,7 @@ func TestPrefixToRoutingMapping(t *testing.T) { t.Run("Producer multihoming — producer reachable via multiple egress routers", func(t *testing.T) { // A producer announces the same prefix to two different routers. // The mapping protocol creates egress entries for both. - bift := &fw.BiftState{} + bift := &bier.BiftState{} router1 := enc.Name{enc.NewGenericComponent("edge-1")} router2 := enc.Name{enc.NewGenericComponent("edge-2")} @@ -197,13 +197,13 @@ func TestPrefixToRoutingMapping(t *testing.T) { egressRouters := []enc.Name{router1, router2} bs := bift.BuildBierBitString(egressRouters) - if !fw.BierGetBit(bs, 0) { + if !bier.BierGetBit(bs, 0) { t.Error("multihomed router1 (bit 0) should be set") } - if !fw.BierGetBit(bs, 1) { + if !bier.BierGetBit(bs, 1) { t.Error("multihomed router2 (bit 1) should be set") } - if fw.BierGetBit(bs, 2) { + if bier.BierGetBit(bs, 2) { t.Error("router3 (bit 2) should NOT be set — producer not attached there") } }) @@ -229,7 +229,7 @@ func TestBfirEncodingFromEgressRouters(t *testing.T) { }) t.Run("BFIR encoding preserves per-router bit accuracy", func(t *testing.T) { - bift := &fw.BiftState{} + bift := &bier.BiftState{} // Register routers at specific BFR-IDs (operator-assigned) names := make([]enc.Name, 10) @@ -246,11 +246,11 @@ func TestBfirEncodingFromEgressRouters(t *testing.T) { expected := map[int]bool{0: true, 3: true, 7: true, 9: true} for i := 0; i < 10; i++ { if expected[i] { - if !fw.BierGetBit(bs, i) { + if !bier.BierGetBit(bs, i) { t.Errorf("BFIR encoding: bit %d should be set (egress router present)", i) } } else { - if fw.BierGetBit(bs, i) { + if bier.BierGetBit(bs, i) { t.Errorf("BFIR encoding: bit %d should NOT be set (no egress router)", i) } } @@ -258,7 +258,7 @@ func TestBfirEncodingFromEgressRouters(t *testing.T) { }) t.Run("BFIR with no valid egress routers returns nil", func(t *testing.T) { - bift := &fw.BiftState{} + bift := &bier.BiftState{} // No routers registered — empty mapping bs := bift.BuildBierBitString(nil) if bs != nil { diff --git a/fw/bier_tests/bier_fib_rebuild_test.go b/fw/bier/bier_fib_rebuild_test.go similarity index 97% rename from fw/bier_tests/bier_fib_rebuild_test.go rename to fw/bier/bier_fib_rebuild_test.go index 09e4c682..f011281f 100644 --- a/fw/bier_tests/bier_fib_rebuild_test.go +++ b/fw/bier/bier_fib_rebuild_test.go @@ -1,4 +1,4 @@ -package bier_tests +package bier_test // TestBiftRebuildOnFibChange verifies that the BIFT is properly rebuilt when // the FIB changes. This tests the hook added in table_algo.go:updateFib(). @@ -12,7 +12,7 @@ package bier_tests import ( "testing" - fw "github.com/named-data/ndnd/fw/fw" + bier "github.com/named-data/ndnd/fw/bier" "github.com/named-data/ndnd/fw/table" enc "github.com/named-data/ndnd/std/encoding" ) @@ -24,7 +24,7 @@ func TestBiftRebuildOnFibChange(t *testing.T) { // Use the global FibStrategyTable (requires initialization). table.Initialize() - b := &fw.BiftState{} + b := &bier.BiftState{} rA := enc.Name{enc.NewGenericComponent("routerA")} rB := enc.Name{enc.NewGenericComponent("routerB")} @@ -96,10 +96,10 @@ func TestBiftRebuildOnFibChange(t *testing.T) { // After the "FIB convergence" above, the bit-string built for routerA // should still result in bit 1 being set (BFR-ID is stable). bs := b.BuildBierBitString([]enc.Name{rA}) - if !fw.BierGetBit(bs, 1) { + if !bier.BierGetBit(bs, 1) { t.Error("bit 1 (routerA BFR-ID) should be set after FIB-driven rebuild") } - if fw.BierGetBit(bs, 2) { + if bier.BierGetBit(bs, 2) { t.Error("bit 2 (routerB BFR-ID) should NOT be set when only routerA is egress") } }) @@ -111,7 +111,7 @@ func TestBiftRebuildOnFibChange(t *testing.T) { func TestBiftBuildFromFibMultipleRebuildsSafe(t *testing.T) { table.Initialize() - b := &fw.BiftState{} + b := &bier.BiftState{} rX := enc.Name{enc.NewGenericComponent("routerX")} b.RegisterRouter(rX, 5) b.UpdateNextHop(5, 77) @@ -128,7 +128,7 @@ func TestBiftBuildFromFibMultipleRebuildsSafe(t *testing.T) { for _, n := range neighbors { if n.FaceID == 77 { found = true - if !fw.BierGetBit(n.Fbm, 5) { + if !bier.BierGetBit(n.Fbm, 5) { t.Error("F-BM for face 77 should have bit 5 set after repeated rebuilds") } } diff --git a/fw/bier_tests/bier_integration_test.go b/fw/bier/bier_integration_test.go similarity index 79% rename from fw/bier_tests/bier_integration_test.go rename to fw/bier/bier_integration_test.go index cd4d0577..3b9674ee 100644 --- a/fw/bier_tests/bier_integration_test.go +++ b/fw/bier/bier_integration_test.go @@ -1,11 +1,11 @@ -package bier_tests +package bier_test import ( "sync" "testing" + bier "github.com/named-data/ndnd/fw/bier" "github.com/named-data/ndnd/fw/core" - fw "github.com/named-data/ndnd/fw/fw" "github.com/named-data/ndnd/fw/table" enc "github.com/named-data/ndnd/std/encoding" ) @@ -21,31 +21,31 @@ func setBierIndex(idx int) func() { func TestBierBitManipulationEdgeCases(t *testing.T) { t.Run("GetBit on empty slice returns false", func(t *testing.T) { - if fw.BierGetBit(nil, 0) { + if bier.BierGetBit(nil, 0) { t.Error("GetBit on nil should be false") } - if fw.BierGetBit([]byte{}, 7) { + if bier.BierGetBit([]byte{}, 7) { t.Error("GetBit on empty slice should be false") } }) t.Run("GetBit out-of-bounds returns false", func(t *testing.T) { bs := []byte{0xFF} // only byte 0 - if fw.BierGetBit(bs, 8) { + if bier.BierGetBit(bs, 8) { t.Error("GetBit at byte 1 of 1-byte slice should be false") } - if fw.BierGetBit(bs, 100) { + if bier.BierGetBit(bs, 100) { t.Error("GetBit far out of bounds should be false") } }) t.Run("SetBit auto-extends slice", func(t *testing.T) { var bs []byte - bs = fw.BierSetBit(bs, 23) // byte index 2 + bs = bier.BierSetBit(bs, 23) // byte index 2 if len(bs) < 3 { t.Errorf("slice should be at least 3 bytes, got %d", len(bs)) } - if !fw.BierGetBit(bs, 23) { + if !bier.BierGetBit(bs, 23) { t.Error("bit 23 should be set") } // Preceding bytes should be zero @@ -56,7 +56,7 @@ func TestBierBitManipulationEdgeCases(t *testing.T) { t.Run("SetBit boundary — bit 7 is MSB of first byte", func(t *testing.T) { var bs []byte - bs = fw.BierSetBit(bs, 7) + bs = bier.BierSetBit(bs, 7) if bs[0] != 0x80 { t.Errorf("expected 0x80, got %02x", bs[0]) } @@ -64,7 +64,7 @@ func TestBierBitManipulationEdgeCases(t *testing.T) { t.Run("SetBit boundary — bit 8 is LSB of second byte", func(t *testing.T) { var bs []byte - bs = fw.BierSetBit(bs, 8) + bs = bier.BierSetBit(bs, 8) if len(bs) < 2 || bs[1] != 0x01 { t.Errorf("expected second byte 0x01, got %v", bs) } @@ -72,20 +72,20 @@ func TestBierBitManipulationEdgeCases(t *testing.T) { t.Run("ClearBit out-of-bounds is a no-op", func(t *testing.T) { bs := []byte{0xFF} - fw.BierClearBit(bs, 100) // should not panic + bier.BierClearBit(bs, 100) // should not panic if bs[0] != 0xFF { t.Error("ClearBit out-of-bounds should not modify the slice") } }) t.Run("ClearBit on nil is a no-op", func(t *testing.T) { - fw.BierClearBit(nil, 0) // must not panic + bier.BierClearBit(nil, 0) // must not panic }) - t.Run("fw.BierAnd with different length slices", func(t *testing.T) { + t.Run("bier.BierAnd with different length slices", func(t *testing.T) { a := []byte{0xFF, 0xFF, 0xFF} // 3 bytes b := []byte{0x0F, 0xF0} // 2 bytes — shorter - res := fw.BierAnd(a, b) + res := bier.BierAnd(a, b) if len(res) != 2 { t.Errorf("result length should be min(3,2)=2, got %d", len(res)) } @@ -94,17 +94,17 @@ func TestBierBitManipulationEdgeCases(t *testing.T) { } }) - t.Run("fw.BierAnd both nil/empty returns empty", func(t *testing.T) { - res := fw.BierAnd(nil, nil) + t.Run("bier.BierAnd both nil/empty returns empty", func(t *testing.T) { + res := bier.BierAnd(nil, nil) if len(res) != 0 { t.Errorf("AND of two nils should be empty") } }) - t.Run("fw.BierAndNot shorter mask", func(t *testing.T) { + t.Run("bier.BierAndNot shorter mask", func(t *testing.T) { a := []byte{0xFF, 0xFF} // 2 bytes b := []byte{0x0F} // 1 byte — shorter - res := fw.BierAndNot(a, b) + res := bier.BierAndNot(a, b) // Only first byte gets bits cleared if res[0] != 0xF0 { t.Errorf("byte 0: expected 0xF0, got %02x", res[0]) @@ -114,26 +114,26 @@ func TestBierBitManipulationEdgeCases(t *testing.T) { } }) - t.Run("fw.BierIsZero on nil is true", func(t *testing.T) { - if !fw.BierIsZero(nil) { + t.Run("bier.BierIsZero on nil is true", func(t *testing.T) { + if !bier.BierIsZero(nil) { t.Error("nil bitstring should be zero") } }) - t.Run("fw.BierIsZero on empty slice is true", func(t *testing.T) { - if !fw.BierIsZero([]byte{}) { + t.Run("bier.BierIsZero on empty slice is true", func(t *testing.T) { + if !bier.BierIsZero([]byte{}) { t.Error("empty bitstring should be zero") } }) - t.Run("fw.BierClone of nil returns nil", func(t *testing.T) { - if fw.BierClone(nil) != nil { + t.Run("bier.BierClone of nil returns nil", func(t *testing.T) { + if bier.BierClone(nil) != nil { t.Error("clone of nil should be nil") } }) - t.Run("fw.BierClone of empty slice returns empty (not nil)", func(t *testing.T) { - c := fw.BierClone([]byte{}) + t.Run("bier.BierClone of empty slice returns empty (not nil)", func(t *testing.T) { + c := bier.BierClone([]byte{}) if c == nil { t.Error("clone of empty slice should be non-nil") } @@ -146,41 +146,41 @@ func TestBierBitManipulationEdgeCases(t *testing.T) { var bs []byte positions := []int{0, 63, 64, 127, 255} for _, pos := range positions { - bs = fw.BierSetBit(bs, pos) + bs = bier.BierSetBit(bs, pos) } for _, pos := range positions { - if !fw.BierGetBit(bs, pos) { + if !bier.BierGetBit(bs, pos) { t.Errorf("bit %d should be set", pos) } } // Adjacent bits should be clear - if fw.BierGetBit(bs, 1) { + if bier.BierGetBit(bs, 1) { t.Error("bit 1 should not be set") } - if fw.BierGetBit(bs, 62) { + if bier.BierGetBit(bs, 62) { t.Error("bit 62 should not be set") } }) } -// --- fw.IsBierEnabled / fw.CfgBierIndex --- +// --- bier.IsBierEnabled / bier.CfgBierIndex --- func TestBierEnabledConfig(t *testing.T) { t.Run("disabled when BierIndex is -1 (default)", func(t *testing.T) { restore := setBierIndex(-1) defer restore() - if fw.IsBierEnabled() { + if bier.IsBierEnabled() { t.Error("BIER should be disabled when BierIndex=-1") } - if fw.CfgBierIndex() != -1 { - t.Error("fw.CfgBierIndex should return -1") + if bier.CfgBierIndex() != -1 { + t.Error("bier.CfgBierIndex should return -1") } }) t.Run("enabled when BierIndex is 0", func(t *testing.T) { restore := setBierIndex(0) defer restore() - if !fw.IsBierEnabled() { + if !bier.IsBierEnabled() { t.Error("BIER should be enabled when BierIndex=0") } }) @@ -188,7 +188,7 @@ func TestBierEnabledConfig(t *testing.T) { t.Run("enabled for large index", func(t *testing.T) { restore := setBierIndex(255) defer restore() - if !fw.IsBierEnabled() { + if !bier.IsBierEnabled() { t.Error("BIER should be enabled when BierIndex=255") } }) @@ -198,7 +198,7 @@ func TestBierEnabledConfig(t *testing.T) { func TestBiftEdgeCases(t *testing.T) { t.Run("GetNeighborEntries on empty BIFT", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} neighbors := b.GetNeighborEntries() if len(neighbors) != 0 { t.Errorf("empty BIFT should have 0 neighbors, got %d", len(neighbors)) @@ -206,7 +206,7 @@ func TestBiftEdgeCases(t *testing.T) { }) t.Run("GetNeighborEntries skips entries with no next hop", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} r := enc.Name{enc.NewGenericComponent("r")} b.RegisterRouter(r, 0) // No UpdateNextHop call — NextHop is 0 @@ -218,7 +218,7 @@ func TestBiftEdgeCases(t *testing.T) { }) t.Run("GetNeighborEntries skips entries with nil F-BM", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} r := enc.Name{enc.NewGenericComponent("r")} b.RegisterRouter(r, 1) b.UpdateNextHop(1, 99) @@ -231,12 +231,12 @@ func TestBiftEdgeCases(t *testing.T) { }) t.Run("RebuildFbm on empty BIFT does not panic", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} b.RebuildFbm() // must not panic }) t.Run("BuildBierBitString with empty egress list returns nil", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} bs := b.BuildBierBitString(nil) if bs != nil { t.Errorf("empty egress list should return nil, got %v", bs) @@ -244,7 +244,7 @@ func TestBiftEdgeCases(t *testing.T) { }) t.Run("BuildBierBitString with all unknown routers returns nil", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} unknown := enc.Name{enc.NewGenericComponent("unknown")} bs := b.BuildBierBitString([]enc.Name{unknown}) if bs != nil { @@ -253,7 +253,7 @@ func TestBiftEdgeCases(t *testing.T) { }) t.Run("RegisterRouter overwrites existing BFR-ID", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} r := enc.Name{enc.NewGenericComponent("router")} b.RegisterRouter(r, 3) b.RegisterRouter(r, 7) // re-register same name, different bit @@ -268,12 +268,12 @@ func TestBiftEdgeCases(t *testing.T) { }) t.Run("UpdateNextHop on non-existent BFR-ID is a no-op", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} b.UpdateNextHop(99, 100) // must not panic }) t.Run("RebuildFbm groups multiple BFR-IDs per face", func(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} for i := 0; i < 8; i++ { name := enc.Name{enc.NewGenericComponent("r" + string(rune('0'+i)))} b.RegisterRouter(name, i) @@ -287,7 +287,7 @@ func TestBiftEdgeCases(t *testing.T) { } fbm := neighbors[0].Fbm for i := 0; i < 8; i++ { - if !fw.BierGetBit(fbm, i) { + if !bier.BierGetBit(fbm, i) { t.Errorf("F-BM for face 555 should have bit %d set", i) } } @@ -298,7 +298,7 @@ func TestBiftEdgeCases(t *testing.T) { // Use table.Initialize() to set it up with the default config. table.Initialize() - b := &fw.BiftState{} + b := &bier.BiftState{} r := enc.Name{enc.NewGenericComponent("r")} b.RegisterRouter(r, 0) b.BuildFromFib() // FIB empty → no next hops resolved, no panic @@ -309,7 +309,7 @@ func TestBiftEdgeCases(t *testing.T) { // --- Concurrent access --- func TestBiftConcurrency(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} var wg sync.WaitGroup const goroutines = 20 @@ -355,7 +355,7 @@ func TestBiftConcurrency(t *testing.T) { } func TestBiftBuildBierBitStringMixed(t *testing.T) { - b := &fw.BiftState{} + b := &bier.BiftState{} known := enc.Name{enc.NewGenericComponent("known")} unknown := enc.Name{enc.NewGenericComponent("unknown")} @@ -364,10 +364,10 @@ func TestBiftBuildBierBitStringMixed(t *testing.T) { bs := b.BuildBierBitString(egressRouters) // Only bit 3 should be set (unknown skipped) - if !fw.BierGetBit(bs, 3) { + if !bier.BierGetBit(bs, 3) { t.Error("bit 3 should be set for known router") } - if fw.BierGetBit(bs, 0) || fw.BierGetBit(bs, 1) || fw.BierGetBit(bs, 2) { + if bier.BierGetBit(bs, 0) || bier.BierGetBit(bs, 1) || bier.BierGetBit(bs, 2) { t.Error("only bit 3 should be set") } } @@ -375,19 +375,19 @@ func TestBiftBuildBierBitStringMixed(t *testing.T) { func TestBierAndNotDoesNotModifyInputs(t *testing.T) { a := []byte{0xFF, 0xFF} b := []byte{0x0F, 0xF0} - aCopy := fw.BierClone(a) - bCopy := fw.BierClone(b) + aCopy := bier.BierClone(a) + bCopy := bier.BierClone(b) - fw.BierAndNot(a, b) + bier.BierAndNot(a, b) for i := range a { if a[i] != aCopy[i] { - t.Errorf("fw.BierAndNot mutated a at byte %d", i) + t.Errorf("bier.BierAndNot mutated a at byte %d", i) } } for i := range b { if b[i] != bCopy[i] { - t.Errorf("fw.BierAndNot mutated b at byte %d", i) + t.Errorf("bier.BierAndNot mutated b at byte %d", i) } } } @@ -395,19 +395,19 @@ func TestBierAndNotDoesNotModifyInputs(t *testing.T) { func TestBierAndDoesNotModifyInputs(t *testing.T) { a := []byte{0xAA, 0xBB} b := []byte{0xCC, 0xDD} - aCopy := fw.BierClone(a) - bCopy := fw.BierClone(b) + aCopy := bier.BierClone(a) + bCopy := bier.BierClone(b) - fw.BierAnd(a, b) + bier.BierAnd(a, b) for i := range a { if a[i] != aCopy[i] { - t.Errorf("fw.BierAnd mutated a at byte %d", i) + t.Errorf("bier.BierAnd mutated a at byte %d", i) } } for i := range b { if b[i] != bCopy[i] { - t.Errorf("fw.BierAnd mutated b at byte %d", i) + t.Errorf("bier.BierAnd mutated b at byte %d", i) } } } diff --git a/fw/bier_tests/bier_test.go b/fw/bier/bier_test.go similarity index 77% rename from fw/bier_tests/bier_test.go rename to fw/bier/bier_test.go index 4d039897..28e314cd 100644 --- a/fw/bier_tests/bier_test.go +++ b/fw/bier/bier_test.go @@ -1,9 +1,9 @@ -package bier_tests +package bier_test import ( "testing" - fw "github.com/named-data/ndnd/fw/fw" + bier "github.com/named-data/ndnd/fw/bier" enc "github.com/named-data/ndnd/std/encoding" ) @@ -13,26 +13,26 @@ func TestBierBitManipulation(t *testing.T) { var bs []byte // Set bits 0, 5, 15 - bs = fw.BierSetBit(bs, 0) - bs = fw.BierSetBit(bs, 5) - bs = fw.BierSetBit(bs, 15) + bs = bier.BierSetBit(bs, 0) + bs = bier.BierSetBit(bs, 5) + bs = bier.BierSetBit(bs, 15) // Check that bits are set correctly - if !fw.BierGetBit(bs, 0) { + if !bier.BierGetBit(bs, 0) { t.Error("Bit 0 should be set") } - if !fw.BierGetBit(bs, 5) { + if !bier.BierGetBit(bs, 5) { t.Error("Bit 5 should be set") } - if !fw.BierGetBit(bs, 15) { + if !bier.BierGetBit(bs, 15) { t.Error("Bit 15 should be set") } // Check that other bits are not set - if fw.BierGetBit(bs, 1) { + if bier.BierGetBit(bs, 1) { t.Error("Bit 1 should not be set") } - if fw.BierGetBit(bs, 7) { + if bier.BierGetBit(bs, 7) { t.Error("Bit 7 should not be set") } }) @@ -40,28 +40,28 @@ func TestBierBitManipulation(t *testing.T) { t.Run("ClearBit", func(t *testing.T) { bs := []byte{0xFF, 0xFF} // All bits set in first two bytes - fw.BierClearBit(bs, 3) - fw.BierClearBit(bs, 10) + bier.BierClearBit(bs, 3) + bier.BierClearBit(bs, 10) - if fw.BierGetBit(bs, 3) { + if bier.BierGetBit(bs, 3) { t.Error("Bit 3 should be cleared") } - if fw.BierGetBit(bs, 10) { + if bier.BierGetBit(bs, 10) { t.Error("Bit 10 should be cleared") } - if !fw.BierGetBit(bs, 2) { + if !bier.BierGetBit(bs, 2) { t.Error("Bit 2 should still be set") } - if !fw.BierGetBit(bs, 9) { + if !bier.BierGetBit(bs, 9) { t.Error("Bit 9 should still be set") } }) - t.Run("fw.BierAnd", func(t *testing.T) { + t.Run("bier.BierAnd", func(t *testing.T) { a := []byte{0b11110000, 0b10101010} b := []byte{0b11001100, 0b11110000} - result := fw.BierAnd(a, b) + result := bier.BierAnd(a, b) expected := []byte{0b11000000, 0b10100000} if len(result) != len(expected) { @@ -74,11 +74,11 @@ func TestBierBitManipulation(t *testing.T) { } }) - t.Run("fw.BierAndNot", func(t *testing.T) { + t.Run("bier.BierAndNot", func(t *testing.T) { a := []byte{0b11111111, 0b11111111} b := []byte{0b00001111, 0b11110000} - result := fw.BierAndNot(a, b) + result := bier.BierAndNot(a, b) expected := []byte{0b11110000, 0b00001111} if len(result) != len(expected) { @@ -91,21 +91,21 @@ func TestBierBitManipulation(t *testing.T) { } }) - t.Run("fw.BierIsZero", func(t *testing.T) { + t.Run("bier.BierIsZero", func(t *testing.T) { zero := []byte{0, 0, 0} nonZero := []byte{0, 0, 1} - if !fw.BierIsZero(zero) { + if !bier.BierIsZero(zero) { t.Error("Zero bitstring should return true") } - if fw.BierIsZero(nonZero) { + if bier.BierIsZero(nonZero) { t.Error("Non-zero bitstring should return false") } }) - t.Run("fw.BierClone", func(t *testing.T) { + t.Run("bier.BierClone", func(t *testing.T) { original := []byte{1, 2, 3, 4} - cloned := fw.BierClone(original) + cloned := bier.BierClone(original) if len(cloned) != len(original) { t.Errorf("Clone length mismatch: got %d, want %d", len(cloned), len(original)) @@ -128,7 +128,7 @@ func TestBierBitManipulation(t *testing.T) { // TestBiftConstruction tests the BIFT construction and lookup func TestBiftConstruction(t *testing.T) { - bift := &fw.BiftState{} + bift := &bier.BiftState{} t.Run("RegisterRouter", func(t *testing.T) { r1 := enc.Name{enc.NewGenericComponent("router1")} @@ -177,13 +177,13 @@ func TestBiftConstruction(t *testing.T) { bs := bift.BuildBierBitString(egressRouters) // Check that bits 0 and 5 are set - if !fw.BierGetBit(bs, 0) { + if !bier.BierGetBit(bs, 0) { t.Error("Bit 0 (router1) should be set") } - if !fw.BierGetBit(bs, 5) { + if !bier.BierGetBit(bs, 5) { t.Error("Bit 5 (router3) should be set") } - if fw.BierGetBit(bs, 1) { + if bier.BierGetBit(bs, 1) { t.Error("Bit 1 (router2) should not be set") } }) @@ -226,13 +226,13 @@ func TestBiftConstruction(t *testing.T) { if face100Fbm == nil { t.Error("Face 100 neighbor entry not found") } else { - if !fw.BierGetBit(face100Fbm, 0) { + if !bier.BierGetBit(face100Fbm, 0) { t.Error("Face 100 F-BM should have bit 0 set") } - if !fw.BierGetBit(face100Fbm, 1) { + if !bier.BierGetBit(face100Fbm, 1) { t.Error("Face 100 F-BM should have bit 1 set") } - if fw.BierGetBit(face100Fbm, 5) { + if bier.BierGetBit(face100Fbm, 5) { t.Error("Face 100 F-BM should not have bit 5 set") } } @@ -240,10 +240,10 @@ func TestBiftConstruction(t *testing.T) { if face200Fbm == nil { t.Error("Face 200 neighbor entry not found") } else { - if !fw.BierGetBit(face200Fbm, 5) { + if !bier.BierGetBit(face200Fbm, 5) { t.Error("Face 200 F-BM should have bit 5 set") } - if fw.BierGetBit(face200Fbm, 0) { + if bier.BierGetBit(face200Fbm, 0) { t.Error("Face 200 F-BM should not have bit 0 set") } } @@ -258,48 +258,48 @@ func TestBierReplicationMask(t *testing.T) { // Neighbor 3 can reach routers at bits 5, 6 var fbm1, fbm2, fbm3 []byte - fbm1 = fw.BierSetBit(fbm1, 0) - fbm1 = fw.BierSetBit(fbm1, 1) - fbm1 = fw.BierSetBit(fbm1, 2) + fbm1 = bier.BierSetBit(fbm1, 0) + fbm1 = bier.BierSetBit(fbm1, 1) + fbm1 = bier.BierSetBit(fbm1, 2) - fbm2 = fw.BierSetBit(fbm2, 3) - fbm2 = fw.BierSetBit(fbm2, 4) + fbm2 = bier.BierSetBit(fbm2, 3) + fbm2 = bier.BierSetBit(fbm2, 4) - fbm3 = fw.BierSetBit(fbm3, 5) - fbm3 = fw.BierSetBit(fbm3, 6) + fbm3 = bier.BierSetBit(fbm3, 5) + fbm3 = bier.BierSetBit(fbm3, 6) // Incoming BIER bit-string has bits 0, 3, 5 set (3 egress routers) var incoming []byte - incoming = fw.BierSetBit(incoming, 0) - incoming = fw.BierSetBit(incoming, 3) - incoming = fw.BierSetBit(incoming, 5) + incoming = bier.BierSetBit(incoming, 0) + incoming = bier.BierSetBit(incoming, 3) + incoming = bier.BierSetBit(incoming, 5) // Compute replication masks - rep1 := fw.BierAnd(incoming, fbm1) - rep2 := fw.BierAnd(incoming, fbm2) - rep3 := fw.BierAnd(incoming, fbm3) + rep1 := bier.BierAnd(incoming, fbm1) + rep2 := bier.BierAnd(incoming, fbm2) + rep3 := bier.BierAnd(incoming, fbm3) // Neighbor 1 should replicate for bit 0 only - if !fw.BierGetBit(rep1, 0) { + if !bier.BierGetBit(rep1, 0) { t.Error("Neighbor 1 should replicate for bit 0") } - if fw.BierGetBit(rep1, 1) || fw.BierGetBit(rep1, 2) { + if bier.BierGetBit(rep1, 1) || bier.BierGetBit(rep1, 2) { t.Error("Neighbor 1 should not replicate for bits 1 or 2") } // Neighbor 2 should replicate for bit 3 only - if !fw.BierGetBit(rep2, 3) { + if !bier.BierGetBit(rep2, 3) { t.Error("Neighbor 2 should replicate for bit 3") } - if fw.BierGetBit(rep2, 4) { + if bier.BierGetBit(rep2, 4) { t.Error("Neighbor 2 should not replicate for bit 4") } // Neighbor 3 should replicate for bit 5 only - if !fw.BierGetBit(rep3, 5) { + if !bier.BierGetBit(rep3, 5) { t.Error("Neighbor 3 should replicate for bit 5") } - if fw.BierGetBit(rep3, 6) { + if bier.BierGetBit(rep3, 6) { t.Error("Neighbor 3 should not replicate for bit 6") } } diff --git a/fw/bier_tests/bier_topo_test.go b/fw/bier/bier_topo_test.go similarity index 98% rename from fw/bier_tests/bier_topo_test.go rename to fw/bier/bier_topo_test.go index 6e87f45a..8d62c2ac 100644 --- a/fw/bier_tests/bier_topo_test.go +++ b/fw/bier/bier_topo_test.go @@ -1,4 +1,4 @@ -package bier_tests +package bier_test // Topology-level BIER simulation tests. // @@ -9,7 +9,7 @@ package bier_tests // - Loop suppression prevents infinite forwarding // - Scale: up to 256 routers // -// Each test creates per-router fw.BiftState objects and runs the BIER algorithm +// Each test creates per-router bier.BiftState objects and runs the BIER algorithm // directly (without the global Bift or processBierInterest), allowing full // multi-router simulation in a single test process. @@ -19,7 +19,7 @@ import ( "sort" "testing" - fw "github.com/named-data/ndnd/fw/fw" + bier "github.com/named-data/ndnd/fw/bier" enc "github.com/named-data/ndnd/std/encoding" ) @@ -30,18 +30,18 @@ import ( // topo represents a simulated BIER network with N routers (IDs 0..N-1). type topo struct { n int - adj [][]int // undirected adjacency list - bift []*fw.BiftState // per-router BIFT (index == router ID == BFR-ID) + adj [][]int // undirected adjacency list + bift []*bier.BiftState // per-router BIFT (index == router ID == BFR-ID) } func newTopo(n int) *topo { t := &topo{ n: n, adj: make([][]int, n), - bift: make([]*fw.BiftState, n), + bift: make([]*bier.BiftState, n), } for i := range t.bift { - t.bift[i] = &fw.BiftState{} + t.bift[i] = &bier.BiftState{} } return t } @@ -89,7 +89,7 @@ func (t *topo) buildBifts() { b.RegisterRouter(name, dst) if nh[dst] >= 0 { // Offset face IDs by 1: NextHop=0 is the "unset" sentinel in - // fw.BiftState, so router 0 as a direct neighbour must map to face 1. + // bier.BiftState, so router 0 as a direct neighbour must map to face 1. b.UpdateNextHop(dst, uint64(nh[dst]+1)) } } @@ -101,7 +101,7 @@ func (t *topo) buildBifts() { func (t *topo) buildBitstring(ids ...int) []byte { var bs []byte for _, id := range ids { - bs = fw.BierSetBit(bs, id) + bs = bier.BierSetBit(bs, id) } return bs } @@ -128,7 +128,7 @@ func (t *topo) simulate(srcRouter int, bs []byte) topoSimResult { hopCount int } - queue := []item{{srcRouter, fw.BierClone(bs), -1, 0}} + queue := []item{{srcRouter, bier.BierClone(bs), -1, 0}} // visited prevents processing the same (router, bitstring) state twice. visited := make(map[string]bool) @@ -142,18 +142,18 @@ func (t *topo) simulate(srcRouter int, bs []byte) topoSimResult { } visited[key] = true - remaining := fw.BierClone(cur.bs) + remaining := bier.BierClone(cur.bs) // BFER check: local bit - if fw.BierGetBit(remaining, cur.router) { + if bier.BierGetBit(remaining, cur.router) { res.delivered[cur.router] = true if _, seen := res.hopCounts[cur.router]; !seen { res.hopCounts[cur.router] = cur.hopCount } - fw.BierClearBit(remaining, cur.router) + bier.BierClearBit(remaining, cur.router) } - if fw.BierIsZero(remaining) { + if bier.BierIsZero(remaining) { continue } @@ -164,13 +164,13 @@ func (t *topo) simulate(srcRouter int, bs []byte) topoSimResult { if nbID == cur.inFace { continue } - mask := fw.BierAnd(remaining, nb.Fbm) - if fw.BierIsZero(mask) { + mask := bier.BierAnd(remaining, nb.Fbm) + if bier.BierIsZero(mask) { continue } res.packetsSent++ queue = append(queue, item{nbID, mask, cur.router, cur.hopCount + 1}) - remaining = fw.BierAndNot(remaining, nb.Fbm) + remaining = bier.BierAndNot(remaining, nb.Fbm) } } return res diff --git a/fw/fw/bier_strategy.go b/fw/fw/bier_strategy.go index 29e96dbd..059cd713 100644 --- a/fw/fw/bier_strategy.go +++ b/fw/fw/bier_strategy.go @@ -19,6 +19,7 @@ package fw import ( + "github.com/named-data/ndnd/fw/bier" "github.com/named-data/ndnd/fw/core" "github.com/named-data/ndnd/fw/defn" "github.com/named-data/ndnd/fw/table" @@ -90,14 +91,14 @@ func (s *BierStrategy) AfterReceiveMulticastInterest( ) { if len(packet.Bier) == 0 { core.Log.Trace(s, "BFIR: encoding BIER bit-string", "name", packet.Name, "egress-count", len(petEntry.EgressRouters)) - packet.Bier = Bift.BuildBierBitString(petEntry.EgressRouters) + packet.Bier = bier.Bift.BuildBierBitString(petEntry.EgressRouters) } - if deliveredToLocal && len(packet.Bier) > 0 && IsBierEnabled() { - bs := BierClone(packet.Bier) - BierClearBit(bs, CfgBierIndex()) + if deliveredToLocal && len(packet.Bier) > 0 && bier.IsBierEnabled() { + bs := bier.BierClone(packet.Bier) + bier.BierClearBit(bs, bier.CfgBierIndex()) packet.Bier = bs - if BierIsZero(bs) { + if bier.BierIsZero(bs) { return } } @@ -117,32 +118,32 @@ func bierReplicate( inFace uint64, sendInterest func(*defn.Pkt, table.PitEntry, uint64, uint64) bool, ) { - incomingBs := BierClone(packet.Bier) + incomingBs := bier.BierClone(packet.Bier) // Clear local bit — local delivery was already handled by thread.go. // Also clear it if no local app is registered, to avoid forwarding our // own bit position to downstream neighbors. - if IsBierEnabled() { - localId := CfgBierIndex() - if localId >= 0 && BierGetBit(incomingBs, localId) { - BierClearBit(incomingBs, localId) + if bier.IsBierEnabled() { + localId := bier.CfgBierIndex() + if localId >= 0 && bier.BierGetBit(incomingBs, localId) { + bier.BierClearBit(incomingBs, localId) } } - if BierIsZero(incomingBs) { + if bier.BierIsZero(incomingBs) { return } core.Log.Trace(logCtx, "BIER replication", "name", packet.Name, "bs-len", len(incomingBs)) - neighbors := Bift.GetNeighborEntries() + neighbors := bier.Bift.GetNeighborEntries() for _, neighbor := range neighbors { if neighbor.FaceID == inFace { continue // Never send back on incoming face } - replicationMask := BierAnd(incomingBs, neighbor.Fbm) - if BierIsZero(replicationMask) { + replicationMask := bier.BierAnd(incomingBs, neighbor.Fbm) + if bier.BierIsZero(replicationMask) { continue } @@ -162,8 +163,8 @@ func bierReplicate( sendInterest(clonePkt, pitEntry, neighbor.FaceID, inFace) // Loop suppression: clear forwarded bits from working mask - incomingBs = BierAndNot(incomingBs, neighbor.Fbm) - if BierIsZero(incomingBs) { + incomingBs = bier.BierAndNot(incomingBs, neighbor.Fbm) + if bier.BierIsZero(incomingBs) { break } } diff --git a/fw/fw/thread.go b/fw/fw/thread.go index a63d6043..4d98422c 100644 --- a/fw/fw/thread.go +++ b/fw/fw/thread.go @@ -14,6 +14,7 @@ import ( "sync/atomic" "time" + "github.com/named-data/ndnd/fw/bier" "github.com/named-data/ndnd/fw/core" "github.com/named-data/ndnd/fw/defn" "github.com/named-data/ndnd/fw/dispatch" @@ -302,8 +303,8 @@ func (t *Thread) processIncomingInterest(packet *defn.Pkt) { var petEntry table.PetEntry var petFound bool petLookup := false - if IsBierEnabled() && len(packet.Bier) > 0 { - if BierGetBit(BierClone(packet.Bier), CfgBierIndex()) { + if bier.IsBierEnabled() && len(packet.Bier) > 0 { + if bier.BierGetBit(bier.BierClone(packet.Bier), bier.CfgBierIndex()) { pipeline = fwMulticastEgress } else { pipeline = fwMulticastTransit diff --git a/fw/mgmt/bift.go b/fw/mgmt/bift.go index 3b6bdb14..25af711a 100644 --- a/fw/mgmt/bift.go +++ b/fw/mgmt/bift.go @@ -8,8 +8,8 @@ package mgmt import ( + "github.com/named-data/ndnd/fw/bier" "github.com/named-data/ndnd/fw/core" - "github.com/named-data/ndnd/fw/fw" mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" "github.com/named-data/ndnd/std/types/optional" ) @@ -73,7 +73,7 @@ func (b *BiftModule) registerRouter(interest *Interest) { } bfrId := params.Cost.GetOr(0) - fw.Bift.RegisterRouter(params.Name, int(bfrId)) + bier.Bift.RegisterRouter(params.Name, int(bfrId)) core.Log.Info(b, "Registered BIFT router", "name", params.Name, "bfrId", bfrId) b.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{ @@ -88,7 +88,7 @@ func (b *BiftModule) rebuild(interest *Interest) { return } - fw.Bift.BuildFromFib() + bier.Bift.BuildFromFib() core.Log.Info(b, "Rebuilt BIFT from FIB") b.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{}) From ecf716927f3a0f9dcee8059a3586e58aee559ac4 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 6 Apr 2026 10:01:22 -0700 Subject: [PATCH 34/38] fw: standardize bier strategy name in fw/fw --- fw/fw/{bier_strategy.go => bier.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fw/fw/{bier_strategy.go => bier.go} (100%) diff --git a/fw/fw/bier_strategy.go b/fw/fw/bier.go similarity index 100% rename from fw/fw/bier_strategy.go rename to fw/fw/bier.go From 4439d767ba2e4bde61395ae3a6d07a2c8d233341 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 13 Apr 2026 01:30:08 -0700 Subject: [PATCH 35/38] ci: upload log artifact in zip form for granular unzipping --- .github/workflows/test.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b75a198..bad4cfea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,18 +90,9 @@ jobs: - name: Run e2e tests run: make e2e - - name: Collect minindn logs on failure - if: failure() - run: | - if [ -d /tmp/minindn ]; then - tar -czf /tmp/minindn-logs.tgz -C /tmp minindn - else - echo "No /tmp/minindn directory found" - fi - - - name: Upload minindn logs + - name: Upload minindn logs on failure if: failure() uses: actions/upload-artifact@v4 with: name: minindn-logs - path: /tmp/minindn-logs.tgz + path: /tmp/minindn From a429ecadc6f86dcadd830935005eb10a7e71a13e Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 13 Apr 2026 01:33:20 -0700 Subject: [PATCH 36/38] dv: use execMgmtRetry for adding fib nexthop for registering neighbor faces --- dv/dv/router.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/dv/dv/router.go b/dv/dv/router.go index 087bad84..319b5bf2 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -358,15 +358,10 @@ func (dv *Router) createFaces() { dv.mutex.Unlock() // Add neighbor to localhop neighbors - dv.nfdc.Exec(nfdc.NfdMgmtCmd{ - Module: "fib", - Cmd: "add-nexthop", - Args: &mgmt.ControlArgs{ - Name: neighborsPrefix.Clone(), - Cost: optional.Some(uint64(1)), - FaceId: optional.Some(faceId), - }, - Retries: 3, + dv.execMgmtRetry("fib", "add-nexthop", &mgmt.ControlArgs{ + Name: neighborsPrefix.Clone(), + Cost: optional.Some(uint64(1)), + FaceId: optional.Some(faceId), }) } } From df243eba17aeb0487c28cceeb058bf25d4bc8926 Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 13 Apr 2026 01:34:06 -0700 Subject: [PATCH 37/38] e2e: use info loglevel to not exhaust ci compute/storage --- e2e/fw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/fw.py b/e2e/fw.py index f6af09a0..5cd5f3b5 100644 --- a/e2e/fw.py +++ b/e2e/fw.py @@ -5,7 +5,7 @@ from minindn.apps.application import Application class NDNd_FW(Application): - def __init__(self, node, config={}, logLevel='TRACE', threads=2, network='/minindn', bier_index=-1): + def __init__(self, node, config={}, logLevel='INFO', threads=2, network='/minindn', bier_index=-1): Application.__init__(self, node) if not shutil.which('ndnd'): From 799fcca61cc5b1e3f962c820366f40233d1f8d7c Mon Sep 17 00:00:00 2001 From: r2dev2 Date: Mon, 13 Apr 2026 01:38:26 -0700 Subject: [PATCH 38/38] fw: do not send to same faceid twice in broadcast --- fw/fw/broadcast.go | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/fw/fw/broadcast.go b/fw/fw/broadcast.go index 0e2f80dd..1c5ccc8e 100644 --- a/fw/fw/broadcast.go +++ b/fw/fw/broadcast.go @@ -69,25 +69,44 @@ func (s *BroadcastStrategy) AfterReceiveMulticastInterest( core.Log.Trace(s, "Broadcast multicast dispatch", "name", packet.Name, "deliveredToLocal", deliveredToLocal, + "petEgress", len(petEntry.EgressRouters), + "petNextHops", len(petEntry.NextHops), ) - seen := make(map[uint64]struct{}) - for _, entry := range table.FibStrategyTable.GetAllFIBEntries() { - for _, nextHop := range entry.GetNextHops() { - if _, ok := seen[nextHop.Nexthop]; ok { + + seen := make(map[uint64]bool) + sent := 0 + + for _, egress := range petEntry.EgressRouters { + nextHops := table.FibStrategyTable.FindNextHopsEnc(egress) + + for _, nextHop := range nextHops { + faceID := nextHop.Nexthop + if _, ok := seen[faceID]; ok { continue } - seen[nextHop.Nexthop] = struct{}{} + seen[faceID] = true - if nextHop.Nexthop == packet.IncomingFaceID { + if faceID == packet.IncomingFaceID { continue } - if pitEntry.InRecords()[nextHop.Nexthop] != nil { + if pitEntry.InRecords()[faceID] != nil { continue } - s.SendInterest(packet, pitEntry, nextHop.Nexthop, inFace) + + if s.SendInterest(packet, pitEntry, faceID, inFace) { + sent++ + } } } + + if sent == 0 { + core.Log.Warn(s, "Broadcast multicast had no eligible PET-scoped nexthops", + "name", packet.Name, + "deliveredToLocal", deliveredToLocal, + "petEgress", len(petEntry.EgressRouters), + ) + } } func (s *BroadcastStrategy) BeforeSatisfyInterest(pitEntry table.PitEntry, inFace uint64) {}