diff --git a/docs/dv-control.md b/docs/dv-control.md index 53a95222..b144d1cd 100644 --- a/docs/dv-control.md +++ b/docs/dv-control.md @@ -2,6 +2,56 @@ This is the detailed reference for the ndn-dv routing daemon control tool. +## Routing status dataset + +Each DV router serves a read-only dataset at +`//32=DV/32=STATUS`. For example: + +```sh +ndnd cat /example/a/32=DV/32=STATUS > routing-status.tlv +``` + +The content is a `dv/tlv.RoutingStatus` snapshot of the responding router's +view: its general status and all reachable routers, with each router's +selected next hop, routing cost, and learned prefix announcements with their +multicast flags and validity periods. +Unreachable routers are omitted. Router and prefix entries are sorted by NDN +name. This reports distance-vector reachability, rather than an NLSR +link-state database or a complete graph of physical links. + +Fetch the unversioned name using the object client's `Consume` API. RDR +metadata identifies a versioned, segmented object with a FinalBlockId. +Snapshots have one-second freshness and a bounded history of recent versions +is retained for in-progress segment fetches. Snapshot generation does not run +on the engine's packet-processing loop. + +Secure routers sign metadata and data segments with their configured DV +router key. Consumers should validate using the DV trust schema and their +network's trust anchors. DV routers install PET egress mappings for other +reachable routers' dataset prefixes, so queries can cross multiple upgraded DV hops. +For remote validation, set `trust.UseDataNameFwHint = true` on the consumer's +`security.TrustConfig` unless the certificate names are independently routable. +This sends certificate Interests with the original Data name as a forwarding +hint, allowing them to follow the dataset route back to its producer. The +dataset routes alone do not route certificate names. DV routers already enable +this option on their own trust configuration. +The existing `/localhost/dv/status` summary remains available. +When using an external routing trust schema, add a rule authorizing the status +prefix under the router certificate, as shown in `dv/config/schema.trust`. + +The dataset content has these TLV fields. The complete wire grammar, including +the nested status fields, is specified in the +[routing status dataset specification](../dv/SPEC.md#routing-status-dataset). + +| Field | Type | Value | +|---|---|---| +| Status | `0x1A0` | Existing DV Status fields | +| Router (repeated) | `0x1A2` | Name, NextHop, Cost, and Prefix fields | +| Name | `0x07` | Router or announced prefix name | +| NextHop | `0xCE` | Destination containing the selected router name | +| Cost | `0xD0` | Non-negative integer | +| Prefix (repeated within Router) | `0x130` | Announced Name, optional Multicast, and ValidityPeriod | + ## `ndnd dv link-create` The link-create command creates a new neighbor link. A new permanent face will be created for the neighbor if a matching face does not exist. diff --git a/docs/fw-control.md b/docs/fw-control.md index fd8c0656..5dc0fc2d 100644 --- a/docs/fw-control.md +++ b/docs/fw-control.md @@ -8,6 +8,79 @@ You can also use the [nfdc](https://docs.named-data.net/NFD/24.07/manpages/nfdc. The status command shows general status of the forwarder, including its version, uptime, data structure counters, and global packet counters. +## `ndnd fw status-report` + +The status-report command prints the NDNd status document as XML by default. Use `--format json` for JSON output. It includes general forwarder counters, faces, FIB, RIB, PET, and BIER/BIFT entries, content-store statistics, strategy choices, and DV status when the DV service is available. The DV section includes reachable routers, next hops, costs, and learned prefix announcements from the [DV routing dataset](dv-control.md#routing-status-dataset). Fields that NDNd does not provide, such as NameTree, Measurements, and Nack counters, are not included. + +DV2-specific sections include PET prefix-to-egress mappings, direct next-hop +faces and costs, and multicast flags. The BIER/BIFT section reports whether +BIER is enabled, the local bit index, router-to-BFR-ID mappings, next-hop +faces, and neighbor forwarding masks as bit-position lists. Disabled BIER +is explicit and distinct from enabled bit index zero. Both sections come +from the local forwarder's management datasets; DV routing/PSD information +comes from the signed router dataset when trust anchors are configured. + +The JSON field types are fixed, independent of the current value: + +| Fields | JSON type | +|---|---| +| Cumulative packet/byte counters and content-store hits/misses | Decimal string | +| Face identifiers, FIB/RIB/PET costs, congestion intervals and thresholds | Decimal string | +| Table sizes, neighbor counts, and content-store capacity | Number | +| DV router costs, BIER bit indices, MTUs, and expiration periods in milliseconds | Number | + +Strings preserve exact full-width integers, including zero and small values. +Numbers represent operational quantities such as current table sizes and +bounded DV costs. XML and HTML output retain their existing representations. + +The dataset reader accepts reachable router costs below `16`, using NDNd's existing DV infinity limit. +A dataset containing a cost outside this range is rejected and its DV +routing details are marked unavailable in all report formats; other status +sections remain available. + +## `ndnd fw status-server` + +The status-server command serves a server-rendered HTML status page at `/` and machine-readable reports at `/status.xml` (`application/xml`) and `/status.json` (`application/json`) over HTTP on port 6380. These endpoints use the same encoders as `status-report`. All formats use the same management datasets. Missing DV status or an unavailable routing dataset is explicitly marked, including when querying an older DV router. + +```sh +ndnd fw status-report > status.xml +ndnd fw status-report --format json > status.json +ndnd fw status-server +# Open http://localhost:6380/ +curl http://localhost:6380/status.xml +curl http://localhost:6380/status.json +``` + +Both status commands accept repeatable `--trust-anchor FILE` options for DV +routing dataset validation. Each file contains one or more trusted NDN root +certificates in PEM or binary format; private keys are not required. Metadata +and every data segment are validated against these anchors and a locally +selected trust schema. `--trust-schema FILE` loads a compiled LVS schema; +otherwise the bundled [DV trust schema](../dv/config/schema.trust) is used. +A custom schema requires at least one trust anchor. Missing or invalid files +fail at startup. Schema mismatches reject the routing dataset; there is no +fallback to the bundled schema, `NullSchema`, or unvalidated data after a +custom schema is selected. Certificate retrieval uses the +data name as a forwarding hint. + +```sh +ndnd fw status-report --trust-anchor network-root.cert +ndnd fw status-server --trust-anchor network-root.cert +# Use the deployment's routing schema, including its status dataset rule: +ndnd fw status-server --trust-schema dv-schema.tlv --trust-anchor network-root.cert +``` + +Without trust anchors, or when validation fails, DV routing details are marked +unavailable. There is no unvalidated fallback for the routing dataset. General +forwarder datasets and the local DV summary continue to use the existing +management connection's trust model; these are not authenticated by the DV +schema. + +When DV validation is configured, failures are logged to stderr with the router +name and the underlying retrieval, validation, or parsing error. The report +still includes available forwarder status. Omitting trust anchors intentionally +disables the routing dataset and does not produce a warning. + ## `ndnd fw face-list` The face list command prints the face table, which contains information about faces. @@ -99,6 +172,37 @@ The fib-list command prints the existing FIB entries. The bift-list command prints the current Bit Index Forwarding Table (BIFT) entries, including each router's BFR-ID, next-hop face ID, and forwarding bit mask. +### BIFT management dataset + +`/localhost/nfd/bift/list` serves a segmented TLV dataset, consumed by both +`bift-list` and the status reports. This replaces DV2's previous JSON wire +payload; the forwarder and its consumers must be updated together. JSON +remains available as a report output format through `/status.json`. + +```abnf +BiftStatus = BiftGeneralStatus *BiftEntry *BiftNeighbor +BiftGeneralStatus = BIFT-GENERAL-TYPE TLV-LENGTH [BierIndex] +BiftEntry = BIFT-ENTRY-TYPE TLV-LENGTH BierIndex Name *FaceId *FbmBit +BiftNeighbor = BIFT-NEIGHBOR-TYPE TLV-LENGTH FaceId *FbmBit +BierIndex = BIER-INDEX-TYPE TLV-LENGTH NonNegativeInteger +FaceId = FACE-ID-TYPE TLV-LENGTH NonNegativeInteger +FbmBit = FBM-BIT-TYPE TLV-LENGTH NonNegativeInteger + +BIFT-GENERAL-TYPE = 768 +BIFT-ENTRY-TYPE = 770 +BIFT-NEIGHBOR-TYPE = 772 +BIER-INDEX-TYPE = 774 +FBM-BIT-TYPE = 776 +FACE-ID-TYPE = 105 +``` + +General status is always present. Its absent `BierIndex` indicates disabled +BIER; a present zero indicates enabled index zero. Within a BIFT entry, +`BierIndex` identifies the destination router's BFR-ID. Repeated `FaceId` +fields list next hops. `FbmBit` fields list the set positions of the +forwarding bit mask. Router entries are sorted by BFR-ID, neighbors by +face ID, and mask positions in ascending order. + ## `ndnd fw pet-list` The pet-list command prints the existing PET entries. diff --git a/dv/SPEC.md b/dv/SPEC.md index 981419ea..b69ae405 100644 --- a/dv/SPEC.md +++ b/dv/SPEC.md @@ -27,6 +27,10 @@ Prefix Group SVS = //32=DV/32=PSD/32=svs Prefix Data = //32=DV/32=PSD//t=/seq=/v=0 Prefix Snapshot = //32=DV/32=PSD//t=/32=SNAP/v= +Routing Status Discovery Interest = //32=DV/32=STATUS/32=metadata +Routing Status Metadata Data = //32=DV/32=STATUS/32=metadata/v=/seg=0 +Routing Status Data = //32=DV/32=STATUS/v=/seg= + = router's unique name in the network = globally unique network prefix ``` @@ -70,7 +74,11 @@ ExitRouter = DESTINATION-TYPE TLV-LENGTH Name PrefixOpReset = PREFIX-OP-RESET-TYPE TLV-LENGTH PrefixOpAdd = PREFIX-OP-ADD-TYPE TLV-LENGTH Name - Cost + [Multicast] + [ValidityPeriod] + +Multicast = COST-TYPE 0 +; ValidityPeriod uses the NDN certificate validity-period format. PrefixOpRemove = PREFIX-OP-REMOVE-TYPE TLV-LENGTH Name @@ -80,6 +88,61 @@ PREFIX-OP-ADD-TYPE = 304 PREFIX-OP-REMOVE-TYPE = 306 ``` +### Routing status dataset + +A router serves its routing status as a versioned, segmented object. +An RDR metadata discovery Interest uses `CanBePrefix` and `MustBeFresh`; +the metadata identifies the versioned object name and final segment. +After reassembling the segments, the Data Content has the following format. +`RoutingStatus` has no outer TLV wrapper. + +```abnf +RoutingStatus = Status + *RouterStatus + +Status = STATUS-TYPE TLV-LENGTH + Version + NetworkName + RouterName + NRibEntries + NNeighbors + NFibEntries + NPsdEntries + +Version = VERSION-TYPE TLV-LENGTH *OCTET +NetworkName = NETWORK-NAME-TYPE TLV-LENGTH Name +RouterName = ROUTER-NAME-TYPE TLV-LENGTH Name +NRibEntries = N-RIB-ENTRIES-TYPE TLV-LENGTH NonNegativeInteger +NNeighbors = N-NEIGHBORS-TYPE TLV-LENGTH NonNegativeInteger +NFibEntries = N-FIB-ENTRIES-TYPE TLV-LENGTH NonNegativeInteger +NPsdEntries = N-PSD-ENTRIES-TYPE TLV-LENGTH NonNegativeInteger + +RouterStatus = ROUTER-STATUS-TYPE TLV-LENGTH + Name + NextHop + Cost + *PrefixOpAdd + +VERSION-TYPE = 401 +NETWORK-NAME-TYPE = 403 +ROUTER-NAME-TYPE = 405 +N-RIB-ENTRIES-TYPE = 407 +N-NEIGHBORS-TYPE = 409 +N-FIB-ENTRIES-TYPE = 411 +N-PSD-ENTRIES-TYPE = 413 +STATUS-TYPE = 416 +ROUTER-STATUS-TYPE = 418 +``` + +`Status` identifies the responding router and its network, reports the NDNd +version as a UTF-8 string, and includes its RIB, neighbor, FIB, and prefix-state +entry counts. +Each `RouterStatus` describes a reachable router, its selected next hop and +routing cost, and its learned prefix announcements. `NextHop`, `Cost`, and +`PrefixOpAdd` use the definitions above. Unreachable routers are omitted. +Router entries and each router's prefix entries are sorted by canonical NDN +name order. + ## 4. Protocol Operation ### RIB State diff --git a/dv/config/config.go b/dv/config/config.go index 04575edc..3e8c67ce 100644 --- a/dv/config/config.go +++ b/dv/config/config.go @@ -255,6 +255,11 @@ func (c *Config) NetworkName() enc.Name { return c.networkNameN } +// StatusDatasetPrefix names a router's remotely retrievable DV status dataset. +func StatusDatasetPrefix(router enc.Name) enc.Name { + return router.Clone().Append(enc.NewKeywordComponent("DV"), enc.NewKeywordComponent("STATUS")) +} + // (AI GENERATED DESCRIPTION): Returns the router name (enc.Name) stored in the Config instance. func (c *Config) RouterName() enc.Name { return c.routerNameN diff --git a/dv/config/schema.tlv b/dv/config/schema.tlv index c2b0d2ca..f43f109a 100644 Binary files a/dv/config/schema.tlv and b/dv/config/schema.tlv differ diff --git a/dv/config/schema.trust b/dv/config/schema.trust index e94ef394..2ac69693 100644 --- a/dv/config/schema.trust +++ b/dv/config/schema.trust @@ -8,6 +8,9 @@ // Advertisement data and broadcast #advertisement_data: /"localhop"/#router/"32=DV"/"32=ADV"/_ <= #router_cert +// Read-only routing status dataset +#status_data: #router/"32=DV"/"32=STATUS" <= #router_cert + // Prefix state database #prefix_state: #network/"32=DV"/"32=PSD" // Prefix state database data diff --git a/dv/dv/router.go b/dv/dv/router.go index ccd67248..8fb53605 100644 --- a/dv/dv/router.go +++ b/dv/dv/router.go @@ -38,6 +38,13 @@ type Router struct { // single mutex for all operations mutex sync.Mutex + // Cache a bounded set of versioned status snapshots for segment fetches. + statusMu sync.Mutex + statusUpdated time.Time + statusObjects *storage.MemoryFifoDir + // Dataset prefix mappings to remote egress routers, protected by mutex. + statusRoutes map[uint64]enc.Name + // channel to stop the DV stop chan bool // heartbeat for outgoing Advertisements @@ -123,12 +130,14 @@ func NewRouter(config *config.Config, engine ndn.Engine) (*Router, error) { // Create the DV router dv := &Router{ - engine: engine, - config: config, - trust: trust, - client: object.NewClient(engine, store, trust), - nfdc: nfdc.NewNfdMgmtThread(engine), - mutex: sync.Mutex{}, + engine: engine, + config: config, + trust: trust, + client: object.NewClient(engine, store, trust), + nfdc: nfdc.NewNfdMgmtThread(engine), + mutex: sync.Mutex{}, + statusObjects: storage.NewMemoryFifoDir(32), + statusRoutes: make(map[uint64]enc.Name), } // Initialize advertisement module @@ -266,6 +275,10 @@ func (dv *Router) register() (err error) { return err } + if err = dv.engine.AttachHandler(config.StatusDatasetPrefix(dv.config.RouterName()), dv.onStatusDataset); err != nil { + return err + } + insertPrefix := dv.pfx.InsertionPrefix() err = dv.engine.AttachHandler(insertPrefix, func(args ndn.InterestHandlerArgs) { @@ -283,6 +296,7 @@ func (dv *Router) register() (err error) { dv.pfx.DataPrefix(), dv.config.MgmtPrefix(), insertPrefix, + config.StatusDatasetPrefix(dv.config.RouterName()), } for _, prefix := range pfxs { diff --git a/dv/dv/status_dataset.go b/dv/dv/status_dataset.go new file mode 100644 index 00000000..285d7fa4 --- /dev/null +++ b/dv/dv/status_dataset.go @@ -0,0 +1,119 @@ +package dv + +import ( + "slices" + "time" + + "github.com/named-data/ndnd/dv/config" + "github.com/named-data/ndnd/dv/nfdc" + "github.com/named-data/ndnd/dv/tlv" + enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/log" + "github.com/named-data/ndnd/std/ndn" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" + rdr "github.com/named-data/ndnd/std/ndn/rdr_2024" + "github.com/named-data/ndnd/std/utils" +) + +func (dv *Router) routingStatus() *tlv.RoutingStatus { + snapshot := dv.copyRoutingStatus() + for _, router := range snapshot.Routers { + slices.SortFunc(router.Prefixes, func(a, b *tlv.PrefixOpAdd) int { return a.Name.Compare(b.Name) }) + } + slices.SortFunc(snapshot.Routers, func(a, b *tlv.RouterStatus) int { return a.Name.Compare(b.Name) }) + return snapshot +} + +// Copy names and values while locked; sorting and encoding use only the copy. +func (dv *Router) copyRoutingStatus() *tlv.RoutingStatus { + dv.mutex.Lock() + defer dv.mutex.Unlock() + + prefixes := dv.pfx.SnapshotEntries() + byRouter := make(map[string][]*tlv.PrefixOpAdd) + for _, prefix := range prefixes { + key := prefix.Router.TlvStr() + byRouter[key] = append(byRouter[key], &tlv.PrefixOpAdd{Name: prefix.Name, Multicast: prefix.Multicast, ValidityPeriod: prefix.ValidityPeriod}) + } + snapshot := &tlv.RoutingStatus{Status: &tlv.Status{ + Version: utils.NDNdVersion, + NetworkName: &tlv.Destination{Name: dv.config.NetworkName().Clone()}, + RouterName: &tlv.Destination{Name: dv.config.RouterName().Clone()}, + NRibEntries: uint64(dv.rib.Size()), NNeighbors: uint64(dv.neighbors.Size()), NFibEntries: uint64(dv.fib.Size()), NPsdEntries: uint64(len(prefixes)), + }} + for _, route := range dv.rib.Advert().Entries { + if route.Cost >= config.CostInfinity { + continue + } + router := &tlv.RouterStatus{ + Name: route.Destination.Name.Clone(), Cost: route.Cost, + NextHop: &tlv.Destination{Name: route.NextHop.Name.Clone()}, + } + router.Prefixes = byRouter[router.Name.TlvStr()] + snapshot.Routers = append(snapshot.Routers, router) + } + return snapshot +} + +func (dv *Router) onStatusDataset(args ndn.InterestHandlerArgs) { + prefix := config.StatusDatasetPrefix(dv.config.RouterName()) + name := args.Interest.Name() + discovery := args.Interest.CanBePrefix() && (name.Equal(prefix) || + name.Equal(prefix.Append(enc.NewKeywordComponent(rdr.MetadataKeyword)))) + if !discovery || !dv.statusMu.TryLock() { + dv.replyStatusDataset(args) + return + } + // At most one publisher runs, outside the engine loop. Existing segments + // remain readable while a new snapshot is copied, signed, and stored. + go func() { + defer dv.statusMu.Unlock() + if dv.statusUpdated.IsZero() || time.Since(dv.statusUpdated) >= time.Second { + now := time.Now() + versioned := prefix.WithVersion(utils.MakeTimestamp(now)) + _, err := dv.client.Produce(ndn.ProduceArgs{ + Name: versioned, Content: dv.routingStatus().Encode(), FreshnessPeriod: time.Second, + }) + if err != nil { + log.Warn(dv, "Failed to publish routing status dataset", "err", err) + return + } + dv.statusObjects.Push(versioned) + if err := dv.statusObjects.Evict(dv.client); err != nil { + log.Warn(dv, "Failed to evict routing status dataset", "err", err) + } + dv.statusUpdated = now + } + dv.replyStatusDataset(args) + }() +} + +func (dv *Router) replyStatusDataset(args ndn.InterestHandlerArgs) { + name := args.Interest.Name() + packet, err := dv.client.Store().Get(name, args.Interest.CanBePrefix()) + if err != nil || packet == nil { + return + } + if err := args.Reply(enc.Wire{packet}); err != nil { + log.Debug(dv, "Failed to reply with routing status dataset", "err", err) + } +} + +// Called with the router mutex held, alongside the normal FIB update. +func (dv *Router) updateStatusRoutes(reachable map[uint64]enc.Name) { + for hash, name := range reachable { + if _, exists := dv.statusRoutes[hash]; !exists { + dv.nfdc.Exec(nfdc.NfdMgmtCmd{Module: "pet", Cmd: "add-egress", Args: &mgmt.ControlArgs{ + Name: config.StatusDatasetPrefix(name), Egress: &mgmt.EgressRecord{Name: name.Clone()}, + }, Retries: -1}) + } + } + for hash, name := range dv.statusRoutes { + if _, exists := reachable[hash]; !exists { + dv.nfdc.Exec(nfdc.NfdMgmtCmd{Module: "pet", Cmd: "remove-egress", Args: &mgmt.ControlArgs{ + Name: config.StatusDatasetPrefix(name), Egress: &mgmt.EgressRecord{Name: name.Clone()}, + }, Retries: -1}) + } + } + dv.statusRoutes = reachable +} diff --git a/dv/dv/status_dataset_consumer_test.go b/dv/dv/status_dataset_consumer_test.go new file mode 100644 index 00000000..5daebca9 --- /dev/null +++ b/dv/dv/status_dataset_consumer_test.go @@ -0,0 +1,117 @@ +package dv + +import ( + "fmt" + "testing" + "time" + + "github.com/named-data/ndnd/dv/config" + "github.com/named-data/ndnd/dv/tlv" + enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/ndn" + "github.com/named-data/ndnd/std/object" + "github.com/named-data/ndnd/std/object/storage" + sec "github.com/named-data/ndnd/std/security" + "github.com/named-data/ndnd/std/security/keychain" + "github.com/named-data/ndnd/std/security/trust_schema" + "github.com/stretchr/testify/require" +) + +// Transport real encoded packets through a serialized engine queue, with only +// the dataset prefix routable. Certificate Interests must use a forwarding hint. +type statusDatasetConsumerEngine struct { + ndn.Engine + queue chan func() + handler ndn.InterestHandler + dataPrefix enc.Name +} + +func (e *statusDatasetConsumerEngine) Post(fn func()) { e.queue <- fn } + +func (e *statusDatasetConsumerEngine) Express(packet *ndn.EncodedInterest, callback ndn.ExpressCallbackFunc) error { + interest, covered, err := e.Spec().ReadInterest(enc.NewWireView(packet.Wire)) + if err != nil { + return err + } + routed := e.dataPrefix.IsPrefix(interest.Name()) + for _, hint := range interest.ForwardingHint() { + routed = routed || e.dataPrefix.IsPrefix(hint) + } + if !routed { + e.Post(func() { + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultError, Error: fmt.Errorf("no route for certificate Interest")}) + }) + return nil + } + e.Post(func() { + e.handler(ndn.InterestHandlerArgs{Interest: interest, SigCovered: covered, Reply: func(wire enc.Wire) error { + data, sigCovered, err := e.Spec().ReadData(enc.NewWireView(wire)) + if err != nil { + return err + } + e.Post(func() { + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultData, Data: data, RawData: wire, SigCovered: sigCovered}) + }) + return nil + }}) + }) + return nil +} + +func TestRoutingStatusRemoteValidation(t *testing.T) { + for _, hint := range []bool{false, true} { + t.Run(fmt.Sprintf("forwarding-hint=%t", hint), func(t *testing.T) { + router, _ := newStatusTestRouter(t) + addStatusTestPrefixes(t, router, 1000) + prefix := config.StatusDatasetPrefix(router.config.RouterName()) + transport := &statusDatasetConsumerEngine{ + Engine: router.engine, queue: make(chan func(), 512), + dataPrefix: prefix, handler: router.onStatusDataset, + } + // The consumer initially knows only the root, so validation must + // fetch the producer's certificate rather than use its keychain. + consumerStore := storage.NewMemoryStore() + kc := keychain.NewKeyChainMem(consumerStore) + anchors := router.config.TrustAnchorNames() + rootWire, err := router.client.Store().Get(anchors[0], false) + require.NoError(t, err) + require.NoError(t, kc.InsertCert(rootWire)) + schema, err := trust_schema.NewLvsSchema(config.SchemaBytes) + require.NoError(t, err) + trust, err := sec.NewTrustConfig(kc, schema, anchors) + require.NoError(t, err) + trust.UseDataNameFwHint = hint + consumer := object.NewClient(transport, consumerStore, trust) + stop, stopped := make(chan struct{}), make(chan struct{}) + go func() { + defer close(stopped) + for { + select { + case fn := <-transport.queue: + fn() + case <-stop: + return + } + } + }() + defer func() { close(stop); <-stopped }() + result := make(chan ndn.ConsumeState, 1) + consumer.Consume(prefix, func(state ndn.ConsumeState) { result <- state }) + select { + case state := <-result: + if !hint { + require.ErrorContains(t, state.Error(), "no route for certificate Interest") + return + } + require.NoError(t, state.Error()) + dataset, err := tlv.ParseRoutingStatus(enc.NewWireView(state.Content()), false) + require.NoError(t, err) + require.Len(t, dataset.Routers, 1) + require.True(t, router.config.RouterName().Equal(dataset.Status.RouterName.Name)) + require.Len(t, dataset.Routers[0].Prefixes, 1000) + case <-time.After(5 * time.Second): + t.Fatal("remote validation did not complete") + } + }) + } +} diff --git a/dv/dv/status_dataset_test.go b/dv/dv/status_dataset_test.go new file mode 100644 index 00000000..4df58371 --- /dev/null +++ b/dv/dv/status_dataset_test.go @@ -0,0 +1,257 @@ +package dv + +import ( + "fmt" + "sync" + "testing" + "time" + + "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/dv/tlv" + enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/engine" + "github.com/named-data/ndnd/std/engine/face" + "github.com/named-data/ndnd/std/ndn" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" + rdr "github.com/named-data/ndnd/std/ndn/rdr_2024" + spec "github.com/named-data/ndnd/std/ndn/spec_2022" + "github.com/named-data/ndnd/std/object/storage" + sec "github.com/named-data/ndnd/std/security" + "github.com/named-data/ndnd/std/security/keychain" + "github.com/named-data/ndnd/std/security/signer" + "github.com/named-data/ndnd/std/security/trust_schema" + "github.com/stretchr/testify/require" +) + +func statusTestName(t *testing.T, name string) enc.Name { + t.Helper() + result, err := enc.NameFromStr(name) + require.NoError(t, err) + return result +} + +func newStatusTestRouter(t *testing.T) (*Router, ndn.Data) { + t.Helper() + cfg := config.DefaultConfig() + cfg.Network, cfg.Router = "/example", "/example/a" + dir := t.TempDir() + kc, err := keychain.NewKeyChainDir(dir, storage.NewMemoryStore()) + require.NoError(t, err) + rootKey, err := signer.KeygenEd25519(sec.MakeKeyName(statusTestName(t, cfg.Network))) + require.NoError(t, err) + notBefore, notAfter := time.Now().Add(-time.Hour), time.Now().Add(time.Hour) + rootWire, err := sec.SelfSign(sec.SignCertArgs{Signer: rootKey, NotBefore: notBefore, NotAfter: notAfter}) + require.NoError(t, err) + root, _, err := (spec.Spec{}).ReadData(enc.NewWireView(rootWire)) + require.NoError(t, err) + require.NoError(t, kc.InsertCert(rootWire.Join())) + identity := statusTestName(t, cfg.Router).Append(enc.NewKeywordComponent("DV")) + key, err := signer.KeygenEd25519(sec.MakeKeyName(identity)) + require.NoError(t, err) + secret, err := signer.MarshalSecretToData(key) + require.NoError(t, err) + certWire, err := sec.SignCert(sec.SignCertArgs{ + Signer: rootKey, Data: secret, IssuerId: enc.NewGenericComponent("network"), + NotBefore: notBefore, NotAfter: notAfter, + }) + require.NoError(t, err) + require.NoError(t, kc.InsertKey(key)) + require.NoError(t, kc.InsertCert(certWire.Join())) + cfg.KeyChainUri = "dir://" + dir + cfg.TrustSchemaPath = "../config/schema.tlv" + cfg.TrustAnchors = []string{root.Name().String()} + router, err := NewRouter(cfg, engine.NewBasicEngine(face.NewDummyFace())) + require.NoError(t, err) + cert, _, err := (spec.Spec{}).ReadData(enc.NewWireView(certWire)) + require.NoError(t, err) + router.rib.Set(cfg.RouterName(), cfg.RouterName(), 0) + return router, cert +} + +func queryStatusPacket(t *testing.T, router *Router, name enc.Name, prefix bool) ndn.Data { + t.Helper() + packet, err := (spec.Spec{}).MakeInterest(name, &ndn.InterestConfig{CanBePrefix: prefix}, nil, nil) + require.NoError(t, err) + interest, _, err := (spec.Spec{}).ReadInterest(enc.NewWireView(packet.Wire)) + require.NoError(t, err) + reply := make(chan enc.Wire, 1) + router.onStatusDataset(ndn.InterestHandlerArgs{Interest: interest, Reply: func(wire enc.Wire) error { reply <- wire; return nil }}) + select { + case wire := <-reply: + data, _, err := (spec.Spec{}).ReadData(enc.NewWireView(wire)) + require.NoError(t, err) + return data + case <-time.After(2 * time.Second): + t.Fatal("routing status Interest did not receive a reply") + return nil + } +} + +func TestRoutingStatusSnapshot(t *testing.T) { + router, _ := newStatusTestRouter(t) + remote, dead := statusTestName(t, "/example/b"), statusTestName(t, "/example/dead") + router.rib.Set(remote, remote, 2) + router.rib.Set(dead, dead, config.CostInfinity) + prefix := statusTestName(t, "/app/data") + router.pfx.GetRouter(remote).Prefixes[prefix.TlvStr()] = &table.PrefixEntry{Name: prefix, Multicast: true, ValidityPeriod: &spec.ValidityPeriod{NotAfter: "20990101T000000"}} + lastPrefix := statusTestName(t, "/z/data") + router.pfx.GetRouter(remote).Prefixes[lastPrefix.TlvStr()] = &table.PrefixEntry{Name: lastPrefix, Multicast: false} + snapshot := router.routingStatus() + require.Len(t, snapshot.Routers, 2) + require.Equal(t, "/example/a", snapshot.Routers[0].Name.String()) + require.Equal(t, "/example/b", snapshot.Routers[1].Name.String()) + require.Equal(t, uint64(2), snapshot.Routers[1].Cost) + require.Equal(t, "/example/b", snapshot.Routers[1].NextHop.Name.String()) + require.True(t, snapshot.Routers[1].Prefixes[1].Multicast) + require.Equal(t, "20990101T000000", snapshot.Routers[1].Prefixes[1].ValidityPeriod.NotAfter) + require.Equal(t, uint64(2), snapshot.Status.NPsdEntries) + require.Len(t, snapshot.Routers[1].Prefixes, 2) + // NDN canonical ordering compares component lengths before their bytes. + require.Equal(t, "/z/data", snapshot.Routers[1].Prefixes[0].Name.String()) + // Snapshots must own their names after the live-table mutex is released. + prefix[0].Val[0] = 'X' + router.pfx.GetRouter(remote).Prefixes[statusTestName(t, "/app/data").TlvStr()].ValidityPeriod.NotAfter = "20000101T000000" + require.Equal(t, "/app/data", snapshot.Routers[1].Prefixes[1].Name.String()) + require.Equal(t, "20990101T000000", snapshot.Routers[1].Prefixes[1].ValidityPeriod.NotAfter) + wire := snapshot.Encode() + parsed, err := tlv.ParseRoutingStatus(enc.NewWireView(wire), false) + require.NoError(t, err) + require.Equal(t, snapshot, parsed) +} + +func addStatusTestPrefixes(t *testing.T, router *Router, count int) { + t.Helper() + entries := router.pfx.GetRouter(router.config.RouterName()).Prefixes + for i := range count { + name := statusTestName(t, fmt.Sprintf("/application/long-prefix-%04d", i)) + entries[name.TlvStr()] = &table.PrefixEntry{Name: name, Multicast: i%2 == 0} + } +} + +func TestRoutingStatusSegmentedAndSigned(t *testing.T) { + router, cert := newStatusTestRouter(t) + addStatusTestPrefixes(t, router, 1000) + prefix := config.StatusDatasetPrefix(router.config.RouterName()) + metadataName := prefix.Append(enc.NewKeywordComponent(rdr.MetadataKeyword)) + metadata := queryStatusPacket(t, router, metadataName, true) + meta, err := rdr.ParseMetaData(enc.NewWireView(metadata.Content()), false) + require.NoError(t, err) + first := queryStatusPacket(t, router, meta.Name.Append(enc.NewSegmentComponent(0)), false) + last := first.FinalBlockID().Unwrap().NumberVal() + require.Positive(t, last) + var content enc.Wire + for i := uint64(0); i <= last; i++ { + packet := queryStatusPacket(t, router, meta.Name.Append(enc.NewSegmentComponent(i)), false) + stored, err := router.client.Store().Get(packet.Name(), false) + require.NoError(t, err) + _, covered, err := (spec.Spec{}).ReadData(enc.NewBufferView(stored)) + require.NoError(t, err) + valid, err := signer.ValidateData(packet, covered, cert) + require.NoError(t, err) + require.True(t, valid) + content = append(content, packet.Content()...) + } + status, err := tlv.ParseRoutingStatus(enc.NewWireView(content), false) + require.NoError(t, err) + require.Len(t, status.Routers[0].Prefixes, 1000) + // Another discovery within the freshness interval uses the same object. + again := queryStatusPacket(t, router, metadataName, true) + require.True(t, metadata.Name().Equal(again.Name())) + require.Equal(t, 1, router.statusObjects.Count()) +} + +func TestRoutingStatusSchema(t *testing.T) { + router, cert := newStatusTestRouter(t) + schema, err := trust_schema.NewLvsSchema(config.SchemaBytes) + require.NoError(t, err) + require.True(t, schema.Check(config.StatusDatasetPrefix(router.config.RouterName()), cert.Name())) + require.False(t, schema.Check(config.StatusDatasetPrefix(statusTestName(t, "/example/b")), cert.Name())) + require.True(t, schema.Check(router.config.AdvertisementDataPrefix().Append(enc.NewTimestampComponent(1)), cert.Name())) + selected := router.client.SuggestSigner(config.StatusDatasetPrefix(router.config.RouterName())) + require.NotNil(t, selected) + keyName, err := sec.GetKeyNameFromCertName(cert.Name()) + require.NoError(t, err) + require.True(t, keyName.Equal(selected.KeyName())) + require.Nil(t, router.client.SuggestSigner(config.StatusDatasetPrefix(statusTestName(t, "/example/b")))) +} + +func TestRoutingStatusConcurrentUpdates(t *testing.T) { + router, _ := newStatusTestRouter(t) + remote := statusTestName(t, "/example/b") + var work sync.WaitGroup + work.Add(2) + go func() { + defer work.Done() + for i := range 100 { + router.mutex.Lock() + router.rib.Set(remote, remote, uint64(i%3+1)) + router.mutex.Unlock() + } + }() + prefix := statusTestName(t, "/app/concurrent") + go func() { + defer work.Done() + for i := range 100 { + router.pfx.mu.Lock() + router.pfx.pfx.GetRouter(remote).Prefixes[prefix.TlvStr()] = &table.PrefixEntry{Name: prefix.Clone(), Multicast: i%2 == 0} + router.pfx.mu.Unlock() + } + }() + for range 100 { + _ = router.routingStatus().Encode() + } + work.Wait() +} + +type statusRouteTestEngine struct { + ndn.Engine + commands chan nfdc.NfdMgmtCmd +} + +func (e statusRouteTestEngine) ExecMgmtCmd(module, cmd string, args any) (any, error) { + e.commands <- nfdc.NfdMgmtCmd{Module: module, Cmd: cmd, Args: args.(*mgmt.ControlArgs)} + return nil, nil +} + +func TestRoutingStatusRoutesAcrossMultipleHops(t *testing.T) { + router, _ := newStatusTestRouter(t) + commands := make(chan nfdc.NfdMgmtCmd, 64) + router.nfdc = nfdc.NewNfdMgmtThread(statusRouteTestEngine{router.engine, commands}) + router.neighbors = table.NewNeighborTable(router.config, router.nfdc) + router.fib = table.NewFib(router.config, router.nfdc) + stopped := make(chan struct{}) + go func() { router.nfdc.Start(); close(stopped) }() + defer func() { router.nfdc.Stop(); <-stopped }() + + neighbor := statusTestName(t, "/example/b") + remote := statusTestName(t, "/example/c") + _, _ = router.neighbors.Add(neighbor).RecvPing(7, true) + router.rib.Set(neighbor, neighbor, 1) + router.rib.Set(remote, neighbor, 2) + router.updateFib() + prefix := config.StatusDatasetPrefix(remote) + waitFor := func(command string) *mgmt.ControlArgs { + t.Helper() + deadline := time.NewTimer(2 * time.Second) + defer deadline.Stop() + for { + select { + case got := <-commands: + if got.Module == "pet" && got.Cmd == command && got.Args.Name.Equal(prefix) { + return got.Args + } + case <-deadline.C: + t.Fatalf("missing dataset route %s", command) + return nil + } + } + } + registered := waitFor("add-egress") + require.True(t, remote.Equal(registered.Egress.Name)) + router.rib.RemoveNextHop(neighbor) + router.rib.Prune() + router.updateFib() + require.True(t, remote.Equal(waitFor("remove-egress").Egress.Name)) +} diff --git a/dv/dv/table_algo.go b/dv/dv/table_algo.go index 8f7a3059..fee2f049 100644 --- a/dv/dv/table_algo.go +++ b/dv/dv/table_algo.go @@ -136,6 +136,7 @@ func (dv *Router) updateFib() { } } dv.fib.RemoveUnmarked() + dv.updateStatusRoutes(names) // Rebuild the BIFT whenever the FIB changes so BIER forwarding paths // are always consistent with the routing table. diff --git a/dv/tlv/definitions.go b/dv/tlv/definitions.go index ca515198..ef23d782 100644 --- a/dv/tlv/definitions.go +++ b/dv/tlv/definitions.go @@ -90,3 +90,22 @@ type PrefixInsertionInnerContent struct { //+field:natural:optional Cost optional.Optional[uint64] `tlv:"0x6a"` } + +// RoutingStatus is a consistent view of the responding router's DV state. +type RoutingStatus struct { + //+field:struct:Status + Status *Status `tlv:"0x1A0"` + //+field:sequence:*RouterStatus:struct:RouterStatus + Routers []*RouterStatus `tlv:"0x1A2"` +} + +type RouterStatus struct { + //+field:name + Name enc.Name `tlv:"0x07"` + //+field:struct:Destination + NextHop *Destination `tlv:"0xCE"` + //+field:natural + Cost uint64 `tlv:"0xD0"` + //+field:sequence:*PrefixOpAdd:struct:PrefixOpAdd + Prefixes []*PrefixOpAdd `tlv:"0x130"` +} diff --git a/dv/tlv/zz_generated.go b/dv/tlv/zz_generated.go index 628abc15..61fbb8ab 100644 --- a/dv/tlv/zz_generated.go +++ b/dv/tlv/zz_generated.go @@ -2134,3 +2134,536 @@ func ParsePrefixInsertionInnerContent(reader enc.WireView, ignoreCritical bool) context.Init() return context.Parse(reader, ignoreCritical) } + +type RoutingStatusEncoder struct { + Length uint + + Status_encoder StatusEncoder + Routers_subencoder []struct { + Routers_encoder RouterStatusEncoder + } +} + +type RoutingStatusParsingContext struct { + Status_context StatusParsingContext + Routers_context RouterStatusParsingContext +} + +func (encoder *RoutingStatusEncoder) Init(value *RoutingStatus) { + if value.Status != nil { + encoder.Status_encoder.Init(value.Status) + } + { + Routers_l := len(value.Routers) + encoder.Routers_subencoder = make([]struct { + Routers_encoder RouterStatusEncoder + }, Routers_l) + for i := 0; i < Routers_l; i++ { + pseudoEncoder := &encoder.Routers_subencoder[i] + pseudoValue := struct { + Routers *RouterStatus + }{ + Routers: value.Routers[i], + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Routers != nil { + encoder.Routers_encoder.Init(value.Routers) + } + _ = encoder + _ = value + } + } + } + + l := uint(0) + if value.Status != nil { + l += 3 + l += uint(enc.TLNum(encoder.Status_encoder.Length).EncodingLength()) + l += encoder.Status_encoder.Length + } + if value.Routers != nil { + for seq_i, seq_v := range value.Routers { + pseudoEncoder := &encoder.Routers_subencoder[seq_i] + pseudoValue := struct { + Routers *RouterStatus + }{ + Routers: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Routers != nil { + l += 3 + l += uint(enc.TLNum(encoder.Routers_encoder.Length).EncodingLength()) + l += encoder.Routers_encoder.Length + } + _ = encoder + _ = value + } + } + } + encoder.Length = l + +} + +func (context *RoutingStatusParsingContext) Init() { + context.Status_context.Init() + context.Routers_context.Init() +} + +func (encoder *RoutingStatusEncoder) EncodeInto(value *RoutingStatus, buf []byte) { + + pos := uint(0) + + if value.Status != nil { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(416)) + pos += 3 + pos += uint(enc.TLNum(encoder.Status_encoder.Length).EncodeInto(buf[pos:])) + if encoder.Status_encoder.Length > 0 { + encoder.Status_encoder.EncodeInto(value.Status, buf[pos:]) + pos += encoder.Status_encoder.Length + } + } + if value.Routers != nil { + for seq_i, seq_v := range value.Routers { + pseudoEncoder := &encoder.Routers_subencoder[seq_i] + pseudoValue := struct { + Routers *RouterStatus + }{ + Routers: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Routers != nil { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(418)) + pos += 3 + pos += uint(enc.TLNum(encoder.Routers_encoder.Length).EncodeInto(buf[pos:])) + if encoder.Routers_encoder.Length > 0 { + encoder.Routers_encoder.EncodeInto(value.Routers, buf[pos:]) + pos += encoder.Routers_encoder.Length + } + } + _ = encoder + _ = value + } + } + } +} + +func (encoder *RoutingStatusEncoder) Encode(value *RoutingStatus) enc.Wire { + + wire := make(enc.Wire, 1) + wire[0] = make([]byte, encoder.Length) + buf := wire[0] + encoder.EncodeInto(value, buf) + + return wire +} + +func (context *RoutingStatusParsingContext) Parse(reader enc.WireView, ignoreCritical bool) (*RoutingStatus, error) { + + var handled_Status bool = false + var handled_Routers bool = false + + progress := -1 + _ = progress + + value := &RoutingStatus{} + var err error + var startPos int + for { + startPos = reader.Pos() + if startPos >= reader.Length() { + break + } + typ := enc.TLNum(0) + l := enc.TLNum(0) + typ, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + l, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + + err = nil + if handled := false; true { + switch typ { + case 416: + if true { + handled = true + handled_Status = true + value.Status, err = context.Status_context.Parse(reader.Delegate(int(l)), ignoreCritical) + } + case 418: + if true { + handled = true + handled_Routers = true + if value.Routers == nil { + value.Routers = make([]*RouterStatus, 0) + } + { + pseudoValue := struct { + Routers *RouterStatus + }{} + { + value := &pseudoValue + value.Routers, err = context.Routers_context.Parse(reader.Delegate(int(l)), ignoreCritical) + _ = value + } + value.Routers = append(value.Routers, pseudoValue.Routers) + } + progress-- + } + default: + if !ignoreCritical && ((typ <= 31) || ((typ & 1) == 1)) { + return nil, enc.ErrUnrecognizedField{TypeNum: typ} + } + handled = true + err = reader.Skip(int(l)) + } + if err == nil && !handled { + } + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: typ, Err: err} + } + } + } + + startPos = reader.Pos() + err = nil + + if !handled_Status && err == nil { + value.Status = nil + } + if !handled_Routers && err == nil { + // sequence - skip + } + + if err != nil { + return nil, err + } + + return value, nil +} + +func (value *RoutingStatus) Encode() enc.Wire { + encoder := RoutingStatusEncoder{} + encoder.Init(value) + return encoder.Encode(value) +} + +func (value *RoutingStatus) Bytes() []byte { + return value.Encode().Join() +} + +func ParseRoutingStatus(reader enc.WireView, ignoreCritical bool) (*RoutingStatus, error) { + context := RoutingStatusParsingContext{} + context.Init() + return context.Parse(reader, ignoreCritical) +} + +type RouterStatusEncoder struct { + Length uint + + Name_length uint + NextHop_encoder DestinationEncoder + + Prefixes_subencoder []struct { + Prefixes_encoder PrefixOpAddEncoder + } +} + +type RouterStatusParsingContext struct { + NextHop_context DestinationParsingContext + + Prefixes_context PrefixOpAddParsingContext +} + +func (encoder *RouterStatusEncoder) Init(value *RouterStatus) { + if value.Name != nil { + encoder.Name_length = 0 + for _, c := range value.Name { + encoder.Name_length += uint(c.EncodingLength()) + } + } + if value.NextHop != nil { + encoder.NextHop_encoder.Init(value.NextHop) + } + + { + Prefixes_l := len(value.Prefixes) + encoder.Prefixes_subencoder = make([]struct { + Prefixes_encoder PrefixOpAddEncoder + }, Prefixes_l) + for i := 0; i < Prefixes_l; i++ { + pseudoEncoder := &encoder.Prefixes_subencoder[i] + pseudoValue := struct { + Prefixes *PrefixOpAdd + }{ + Prefixes: value.Prefixes[i], + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Prefixes != nil { + encoder.Prefixes_encoder.Init(value.Prefixes) + } + _ = encoder + _ = value + } + } + } + + l := uint(0) + if value.Name != nil { + l += 1 + l += uint(enc.TLNum(encoder.Name_length).EncodingLength()) + l += encoder.Name_length + } + if value.NextHop != nil { + l += 1 + l += uint(enc.TLNum(encoder.NextHop_encoder.Length).EncodingLength()) + l += encoder.NextHop_encoder.Length + } + l += 1 + l += uint(1 + enc.Nat(value.Cost).EncodingLength()) + if value.Prefixes != nil { + for seq_i, seq_v := range value.Prefixes { + pseudoEncoder := &encoder.Prefixes_subencoder[seq_i] + pseudoValue := struct { + Prefixes *PrefixOpAdd + }{ + Prefixes: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Prefixes != nil { + l += 3 + l += uint(enc.TLNum(encoder.Prefixes_encoder.Length).EncodingLength()) + l += encoder.Prefixes_encoder.Length + } + _ = encoder + _ = value + } + } + } + encoder.Length = l + +} + +func (context *RouterStatusParsingContext) Init() { + + context.NextHop_context.Init() + + context.Prefixes_context.Init() +} + +func (encoder *RouterStatusEncoder) EncodeInto(value *RouterStatus, buf []byte) { + + pos := uint(0) + + if value.Name != nil { + buf[pos] = byte(7) + pos += 1 + pos += uint(enc.TLNum(encoder.Name_length).EncodeInto(buf[pos:])) + for _, c := range value.Name { + pos += uint(c.EncodeInto(buf[pos:])) + } + } + if value.NextHop != nil { + buf[pos] = byte(206) + pos += 1 + pos += uint(enc.TLNum(encoder.NextHop_encoder.Length).EncodeInto(buf[pos:])) + if encoder.NextHop_encoder.Length > 0 { + encoder.NextHop_encoder.EncodeInto(value.NextHop, buf[pos:]) + pos += encoder.NextHop_encoder.Length + } + } + buf[pos] = byte(208) + pos += 1 + + buf[pos] = byte(enc.Nat(value.Cost).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + if value.Prefixes != nil { + for seq_i, seq_v := range value.Prefixes { + pseudoEncoder := &encoder.Prefixes_subencoder[seq_i] + pseudoValue := struct { + Prefixes *PrefixOpAdd + }{ + Prefixes: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Prefixes != nil { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(304)) + pos += 3 + pos += uint(enc.TLNum(encoder.Prefixes_encoder.Length).EncodeInto(buf[pos:])) + if encoder.Prefixes_encoder.Length > 0 { + encoder.Prefixes_encoder.EncodeInto(value.Prefixes, buf[pos:]) + pos += encoder.Prefixes_encoder.Length + } + } + _ = encoder + _ = value + } + } + } +} + +func (encoder *RouterStatusEncoder) Encode(value *RouterStatus) enc.Wire { + + wire := make(enc.Wire, 1) + wire[0] = make([]byte, encoder.Length) + buf := wire[0] + encoder.EncodeInto(value, buf) + + return wire +} + +func (context *RouterStatusParsingContext) Parse(reader enc.WireView, ignoreCritical bool) (*RouterStatus, error) { + + var handled_Name bool = false + var handled_NextHop bool = false + var handled_Cost bool = false + var handled_Prefixes bool = false + + progress := -1 + _ = progress + + value := &RouterStatus{} + var err error + var startPos int + for { + startPos = reader.Pos() + if startPos >= reader.Length() { + break + } + typ := enc.TLNum(0) + l := enc.TLNum(0) + typ, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + l, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + + err = nil + if handled := false; true { + switch typ { + case 7: + if true { + handled = true + handled_Name = true + delegate := reader.Delegate(int(l)) + value.Name, err = delegate.ReadName() + } + case 206: + if true { + handled = true + handled_NextHop = true + value.NextHop, err = context.NextHop_context.Parse(reader.Delegate(int(l)), ignoreCritical) + } + case 208: + if true { + handled = true + handled_Cost = true + value.Cost = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.Cost = uint64(value.Cost<<8) | uint64(x) + } + } + } + case 304: + if true { + handled = true + handled_Prefixes = true + if value.Prefixes == nil { + value.Prefixes = make([]*PrefixOpAdd, 0) + } + { + pseudoValue := struct { + Prefixes *PrefixOpAdd + }{} + { + value := &pseudoValue + value.Prefixes, err = context.Prefixes_context.Parse(reader.Delegate(int(l)), ignoreCritical) + _ = value + } + value.Prefixes = append(value.Prefixes, pseudoValue.Prefixes) + } + progress-- + } + default: + if !ignoreCritical && ((typ <= 31) || ((typ & 1) == 1)) { + return nil, enc.ErrUnrecognizedField{TypeNum: typ} + } + handled = true + err = reader.Skip(int(l)) + } + if err == nil && !handled { + } + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: typ, Err: err} + } + } + } + + startPos = reader.Pos() + err = nil + + if !handled_Name && err == nil { + value.Name = nil + } + if !handled_NextHop && err == nil { + value.NextHop = nil + } + if !handled_Cost && err == nil { + err = enc.ErrSkipRequired{Name: "Cost", TypeNum: 208} + } + if !handled_Prefixes && err == nil { + // sequence - skip + } + + if err != nil { + return nil, err + } + + return value, nil +} + +func (value *RouterStatus) Encode() enc.Wire { + encoder := RouterStatusEncoder{} + encoder.Init(value) + return encoder.Encode(value) +} + +func (value *RouterStatus) Bytes() []byte { + return value.Encode().Join() +} + +func ParseRouterStatus(reader enc.WireView, ignoreCritical bool) (*RouterStatus, error) { + context := RouterStatusParsingContext{} + context.Init() + return context.Parse(reader, ignoreCritical) +} diff --git a/fw/bier/bier.go b/fw/bier/bier.go index 45c27b2e..945b9149 100644 --- a/fw/bier/bier.go +++ b/fw/bier/bier.go @@ -8,12 +8,15 @@ package bier import ( + "slices" "sort" "sync" "github.com/named-data/ndnd/fw/core" "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" + "github.com/named-data/ndnd/std/types/optional" ) // BiftEntry represents a single entry in the Bit Index Forwarding Table. @@ -26,27 +29,6 @@ type BiftEntry struct { Fbm []byte // Forwarding Bit Mask for this neighbor } -// BiftStatusEntry is a JSON-friendly snapshot of one BIFT entry. -type BiftStatusEntry struct { - BfrId int `json:"bfr_id"` - RouterName string `json:"router_name"` - NextHops []uint64 `json:"next_hops"` - FbmBits []int `json:"fbm_bits"` -} - -// BiftStatusNeighbor is a JSON-friendly snapshot of one BIFT neighbor entry. -type BiftStatusNeighbor struct { - FaceID uint64 `json:"face_id"` - FbmBits []int `json:"fbm_bits"` -} - -// BiftStatus is a JSON-friendly snapshot of the current BIFT state. -type BiftStatus struct { - BierIndex int `json:"bier_index"` - Entries []BiftStatusEntry `json:"entries"` - Neighbors []BiftStatusNeighbor `json:"neighbors"` -} - // Bift is the global Bit Index Forwarding Table. var Bift = &BiftState{} @@ -291,24 +273,25 @@ func (b *BiftState) BuildFromFib() { b.rebuildFbmLocked() } -func bitPositions(bs []byte) []int { - positions := make([]int, 0) +func bitPositions(bs []byte) []uint64 { + positions := make([]uint64, 0) for byteIdx, value := range bs { if value == 0 { continue } for bitIdx := 0; bitIdx < 8; bitIdx++ { if value&(1<= 0 { + status.General.BierIndex = optional.Some(uint64(index)) } + entries := b.snapshotEntries() + seenFaces := make(map[uint64]struct{}) for _, entry := range entries { - nextHops := make([]uint64, 0, 1) + item := &mgmt.BiftEntry{ + BfrId: uint64(entry.BfrId), RouterName: entry.RouterName, FbmBits: bitPositions(entry.Fbm), + } if entry.NextHop > 0 { - nextHops = append(nextHops, entry.NextHop) + item.NextHops = []uint64{entry.NextHop} + if _, seen := seenFaces[entry.NextHop]; !seen && entry.Fbm != nil { + status.Neighbors = append(status.Neighbors, &mgmt.BiftNeighbor{ + FaceID: entry.NextHop, FbmBits: slices.Clone(item.FbmBits), + }) + seenFaces[entry.NextHop] = struct{}{} + } } - status.Entries = append(status.Entries, BiftStatusEntry{ - BfrId: entry.BfrId, - RouterName: entry.RouterName.String(), - NextHops: nextHops, - FbmBits: bitPositions(entry.Fbm), - }) - } - - neighbors := b.GetNeighborEntries() - sort.Slice(neighbors, func(i, j int) bool { - return neighbors[i].FaceID < neighbors[j].FaceID - }) - status.Neighbors = make([]BiftStatusNeighbor, 0, len(neighbors)) - for _, neighbor := range neighbors { - status.Neighbors = append(status.Neighbors, BiftStatusNeighbor{ - FaceID: neighbor.FaceID, - FbmBits: bitPositions(neighbor.Fbm), - }) + status.Entries = append(status.Entries, item) } - + sort.Slice(status.Entries, func(i, j int) bool { return status.Entries[i].BfrId < status.Entries[j].BfrId }) + sort.Slice(status.Neighbors, func(i, j int) bool { return status.Neighbors[i].FaceID < status.Neighbors[j].FaceID }) return status } diff --git a/fw/bier/bier_status_test.go b/fw/bier/bier_status_test.go new file mode 100644 index 00000000..f5f4d309 --- /dev/null +++ b/fw/bier/bier_status_test.go @@ -0,0 +1,59 @@ +package bier_test + +import ( + "testing" + + "github.com/named-data/ndnd/fw/bier" + enc "github.com/named-data/ndnd/std/encoding" +) + +func TestBiftStatusOwnsSnapshot(t *testing.T) { + state := &bier.BiftState{} + name := enc.Name{enc.NewGenericComponent("router")} + state.RegisterRouter(name, 0) + state.UpdateNextHop(0, 100) + state.RebuildFbm() + + status := state.Status() + status.Entries[0].RouterName[0].Val[0] = 'X' + status.Entries[0].FbmBits[0] = 7 + if status.Neighbors[0].FbmBits[0] != 0 { + t.Fatal("entry and neighbor masks share mutable storage") + } + again := state.Status() + if !again.Entries[0].RouterName.Equal(name) || again.Entries[0].FbmBits[0] != 0 { + t.Fatal("modifying a snapshot changed live BIFT state") + } +} + +func TestBiftStatusConsistentDuringUpdates(t *testing.T) { + state := &bier.BiftState{} + state.RegisterRouter(enc.Name{enc.NewGenericComponent("router")}, 0) + state.UpdateNextHop(0, 100) + state.RebuildFbm() + + stop, started, done := make(chan struct{}), make(chan struct{}), make(chan struct{}) + go func() { + defer close(done) + close(started) + for face := uint64(100); ; face = 300 - face { + select { + case <-stop: + return + default: + state.UpdateNextHop(0, face) + } + } + }() + <-started + defer func() { close(stop); <-done }() + for range 1000 { + status := state.Status() + if len(status.Entries) != 1 || len(status.Neighbors) != 1 { + t.Fatal("snapshot lost its entry or neighbor") + } + if status.Entries[0].NextHops[0] != status.Neighbors[0].FaceID { + t.Fatal("entry and neighbor came from different BIFT states") + } + } +} diff --git a/fw/bier/bier_test.go b/fw/bier/bier_test.go index c1e1653c..11457903 100644 --- a/fw/bier/bier_test.go +++ b/fw/bier/bier_test.go @@ -1,10 +1,12 @@ package bier_test import ( + "fmt" "testing" bier "github.com/named-data/ndnd/fw/bier" enc "github.com/named-data/ndnd/std/encoding" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" ) // TestBierBitManipulation tests the BIER bit manipulation helper functions @@ -270,7 +272,7 @@ func TestBiftConstruction(t *testing.T) { if status.Entries[0].BfrId != 0 || status.Entries[1].BfrId != 1 || status.Entries[2].BfrId != 5 { t.Fatalf("Status entries not sorted by BFR-ID: %+v", status.Entries) } - if status.Entries[0].RouterName != r0.String() { + if !status.Entries[0].RouterName.Equal(r0) { t.Fatalf("Unexpected router name for index 0: got %s want %s", status.Entries[0].RouterName, r0) } if len(status.Entries[0].NextHops) != 1 || status.Entries[0].NextHops[0] != 100 { @@ -354,3 +356,27 @@ func TestBierReplicationMask(t *testing.T) { t.Error("Neighbor 3 should not replicate for bit 6") } } + +func TestBiftStatusTLVIndex(t *testing.T) { + for _, index := range []int{-1, 0, 5} { + t.Run(fmt.Sprintf("index=%d", index), func(t *testing.T) { + defer setBierIndex(index)() + snapshot := &bier.BiftState{} + status := snapshot.Status() + parsed, err := mgmt.ParseBiftStatus(enc.NewWireView(status.Encode()), false) + if err != nil { + t.Fatal(err) + } + if parsed.General == nil { + t.Fatal("missing general status") + } + value, enabled := parsed.General.BierIndex.Get() + if enabled != (index >= 0) { + t.Fatalf("index=%d enabled=%t", index, enabled) + } + if enabled && value != uint64(index) { + t.Fatalf("index=%d encoded as %d", index, value) + } + }) + } +} diff --git a/fw/mgmt/bift.go b/fw/mgmt/bift.go index 55b2d464..71ef02ea 100644 --- a/fw/mgmt/bift.go +++ b/fw/mgmt/bift.go @@ -8,8 +8,6 @@ package mgmt import ( - "encoding/json" - "github.com/named-data/ndnd/fw/bier" "github.com/named-data/ndnd/fw/core" enc "github.com/named-data/ndnd/std/encoding" @@ -105,14 +103,10 @@ func (b *BiftModule) list(interest *Interest) { return } - status, err := json.Marshal(bier.Bift.Status()) - if err != nil { - b.manager.sendCtrlResp(interest, 500, "Unable to encode BIFT status", nil) - return - } + status := bier.Bift.Status() name := LOCAL_PREFIX. Append(enc.NewGenericComponent("bift")). Append(enc.NewGenericComponent("list")) - b.manager.sendStatusDataset(interest, name, enc.Wire{status}) + b.manager.sendStatusDataset(interest, name, status.Encode()) } diff --git a/fw/mgmt/face.go b/fw/mgmt/face.go index c81fb457..a83e5165 100644 --- a/fw/mgmt/face.go +++ b/fw/mgmt/face.go @@ -727,7 +727,7 @@ func (f *FaceModule) createDataset(selectedFace face.LinkService) *mgmt.FaceStat NOutData: selectedFace.NOutData(), NOutNacks: 0, NInBytes: selectedFace.NInBytes(), - NOutBytes: selectedFace.NInBytes(), + NOutBytes: selectedFace.NOutBytes(), } if selectedFace.ExpirationPeriod() != 0 { faceDataset.ExpirationPeriod = optional.Some(uint64(selectedFace.ExpirationPeriod().Milliseconds())) diff --git a/std/ndn/mgmt_2022/definitions.go b/std/ndn/mgmt_2022/definitions.go index 1a1687ed..da165c68 100644 --- a/std/ndn/mgmt_2022/definitions.go +++ b/std/ndn/mgmt_2022/definitions.go @@ -363,3 +363,37 @@ type CsQuery struct { PacketSize uint64 FreshnessPeriod uint64 } + +// BiftStatus is the local BIER forwarding status dataset. +type BiftStatus struct { + //+field:struct:BiftGeneralStatus + General *BiftGeneralStatus `tlv:"0x300"` + //+field:sequence:*BiftEntry:struct:BiftEntry + Entries []*BiftEntry `tlv:"0x302"` + //+field:sequence:*BiftNeighbor:struct:BiftNeighbor + Neighbors []*BiftNeighbor `tlv:"0x304"` +} + +type BiftGeneralStatus struct { + // An absent index means BIER is disabled; index zero is valid. + //+field:natural:optional + BierIndex optional.Optional[uint64] `tlv:"0x306"` +} + +type BiftEntry struct { + //+field:natural + BfrId uint64 `tlv:"0x306"` + //+field:name + RouterName enc.Name `tlv:"0x07"` + //+field:sequence:uint64:natural + NextHops []uint64 `tlv:"0x69"` + //+field:sequence:uint64:natural + FbmBits []uint64 `tlv:"0x308"` +} + +type BiftNeighbor struct { + //+field:natural + FaceID uint64 `tlv:"0x69"` + //+field:sequence:uint64:natural + FbmBits []uint64 `tlv:"0x308"` +} diff --git a/std/ndn/mgmt_2022/zz_generated.go b/std/ndn/mgmt_2022/zz_generated.go index 458933fe..4337a3c8 100644 --- a/std/ndn/mgmt_2022/zz_generated.go +++ b/std/ndn/mgmt_2022/zz_generated.go @@ -2,6 +2,7 @@ package mgmt_2022 import ( + "encoding/binary" "io" "strings" @@ -7260,3 +7261,1080 @@ func ParseCsInfoMsg(reader enc.WireView, ignoreCritical bool) (*CsInfoMsg, error context.Init() return context.Parse(reader, ignoreCritical) } + +type BiftStatusEncoder struct { + Length uint + + General_encoder BiftGeneralStatusEncoder + Entries_subencoder []struct { + Entries_encoder BiftEntryEncoder + } + Neighbors_subencoder []struct { + Neighbors_encoder BiftNeighborEncoder + } +} + +type BiftStatusParsingContext struct { + General_context BiftGeneralStatusParsingContext + Entries_context BiftEntryParsingContext + Neighbors_context BiftNeighborParsingContext +} + +func (encoder *BiftStatusEncoder) Init(value *BiftStatus) { + if value.General != nil { + encoder.General_encoder.Init(value.General) + } + { + Entries_l := len(value.Entries) + encoder.Entries_subencoder = make([]struct { + Entries_encoder BiftEntryEncoder + }, Entries_l) + for i := 0; i < Entries_l; i++ { + pseudoEncoder := &encoder.Entries_subencoder[i] + pseudoValue := struct { + Entries *BiftEntry + }{ + Entries: value.Entries[i], + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Entries != nil { + encoder.Entries_encoder.Init(value.Entries) + } + _ = encoder + _ = value + } + } + } + { + Neighbors_l := len(value.Neighbors) + encoder.Neighbors_subencoder = make([]struct { + Neighbors_encoder BiftNeighborEncoder + }, Neighbors_l) + for i := 0; i < Neighbors_l; i++ { + pseudoEncoder := &encoder.Neighbors_subencoder[i] + pseudoValue := struct { + Neighbors *BiftNeighbor + }{ + Neighbors: value.Neighbors[i], + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Neighbors != nil { + encoder.Neighbors_encoder.Init(value.Neighbors) + } + _ = encoder + _ = value + } + } + } + + l := uint(0) + if value.General != nil { + l += 3 + l += uint(enc.TLNum(encoder.General_encoder.Length).EncodingLength()) + l += encoder.General_encoder.Length + } + if value.Entries != nil { + for seq_i, seq_v := range value.Entries { + pseudoEncoder := &encoder.Entries_subencoder[seq_i] + pseudoValue := struct { + Entries *BiftEntry + }{ + Entries: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Entries != nil { + l += 3 + l += uint(enc.TLNum(encoder.Entries_encoder.Length).EncodingLength()) + l += encoder.Entries_encoder.Length + } + _ = encoder + _ = value + } + } + } + if value.Neighbors != nil { + for seq_i, seq_v := range value.Neighbors { + pseudoEncoder := &encoder.Neighbors_subencoder[seq_i] + pseudoValue := struct { + Neighbors *BiftNeighbor + }{ + Neighbors: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Neighbors != nil { + l += 3 + l += uint(enc.TLNum(encoder.Neighbors_encoder.Length).EncodingLength()) + l += encoder.Neighbors_encoder.Length + } + _ = encoder + _ = value + } + } + } + encoder.Length = l + +} + +func (context *BiftStatusParsingContext) Init() { + context.General_context.Init() + context.Entries_context.Init() + context.Neighbors_context.Init() +} + +func (encoder *BiftStatusEncoder) EncodeInto(value *BiftStatus, buf []byte) { + + pos := uint(0) + + if value.General != nil { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(768)) + pos += 3 + pos += uint(enc.TLNum(encoder.General_encoder.Length).EncodeInto(buf[pos:])) + if encoder.General_encoder.Length > 0 { + encoder.General_encoder.EncodeInto(value.General, buf[pos:]) + pos += encoder.General_encoder.Length + } + } + if value.Entries != nil { + for seq_i, seq_v := range value.Entries { + pseudoEncoder := &encoder.Entries_subencoder[seq_i] + pseudoValue := struct { + Entries *BiftEntry + }{ + Entries: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Entries != nil { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(770)) + pos += 3 + pos += uint(enc.TLNum(encoder.Entries_encoder.Length).EncodeInto(buf[pos:])) + if encoder.Entries_encoder.Length > 0 { + encoder.Entries_encoder.EncodeInto(value.Entries, buf[pos:]) + pos += encoder.Entries_encoder.Length + } + } + _ = encoder + _ = value + } + } + } + if value.Neighbors != nil { + for seq_i, seq_v := range value.Neighbors { + pseudoEncoder := &encoder.Neighbors_subencoder[seq_i] + pseudoValue := struct { + Neighbors *BiftNeighbor + }{ + Neighbors: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + if value.Neighbors != nil { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(772)) + pos += 3 + pos += uint(enc.TLNum(encoder.Neighbors_encoder.Length).EncodeInto(buf[pos:])) + if encoder.Neighbors_encoder.Length > 0 { + encoder.Neighbors_encoder.EncodeInto(value.Neighbors, buf[pos:]) + pos += encoder.Neighbors_encoder.Length + } + } + _ = encoder + _ = value + } + } + } +} + +func (encoder *BiftStatusEncoder) Encode(value *BiftStatus) enc.Wire { + + wire := make(enc.Wire, 1) + wire[0] = make([]byte, encoder.Length) + buf := wire[0] + encoder.EncodeInto(value, buf) + + return wire +} + +func (context *BiftStatusParsingContext) Parse(reader enc.WireView, ignoreCritical bool) (*BiftStatus, error) { + + var handled_General bool = false + var handled_Entries bool = false + var handled_Neighbors bool = false + + progress := -1 + _ = progress + + value := &BiftStatus{} + var err error + var startPos int + for { + startPos = reader.Pos() + if startPos >= reader.Length() { + break + } + typ := enc.TLNum(0) + l := enc.TLNum(0) + typ, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + l, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + + err = nil + if handled := false; true { + switch typ { + case 768: + if true { + handled = true + handled_General = true + value.General, err = context.General_context.Parse(reader.Delegate(int(l)), ignoreCritical) + } + case 770: + if true { + handled = true + handled_Entries = true + if value.Entries == nil { + value.Entries = make([]*BiftEntry, 0) + } + { + pseudoValue := struct { + Entries *BiftEntry + }{} + { + value := &pseudoValue + value.Entries, err = context.Entries_context.Parse(reader.Delegate(int(l)), ignoreCritical) + _ = value + } + value.Entries = append(value.Entries, pseudoValue.Entries) + } + progress-- + } + case 772: + if true { + handled = true + handled_Neighbors = true + if value.Neighbors == nil { + value.Neighbors = make([]*BiftNeighbor, 0) + } + { + pseudoValue := struct { + Neighbors *BiftNeighbor + }{} + { + value := &pseudoValue + value.Neighbors, err = context.Neighbors_context.Parse(reader.Delegate(int(l)), ignoreCritical) + _ = value + } + value.Neighbors = append(value.Neighbors, pseudoValue.Neighbors) + } + progress-- + } + default: + if !ignoreCritical && ((typ <= 31) || ((typ & 1) == 1)) { + return nil, enc.ErrUnrecognizedField{TypeNum: typ} + } + handled = true + err = reader.Skip(int(l)) + } + if err == nil && !handled { + } + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: typ, Err: err} + } + } + } + + startPos = reader.Pos() + err = nil + + if !handled_General && err == nil { + value.General = nil + } + if !handled_Entries && err == nil { + // sequence - skip + } + if !handled_Neighbors && err == nil { + // sequence - skip + } + + if err != nil { + return nil, err + } + + return value, nil +} + +func (value *BiftStatus) Encode() enc.Wire { + encoder := BiftStatusEncoder{} + encoder.Init(value) + return encoder.Encode(value) +} + +func (value *BiftStatus) Bytes() []byte { + return value.Encode().Join() +} + +func ParseBiftStatus(reader enc.WireView, ignoreCritical bool) (*BiftStatus, error) { + context := BiftStatusParsingContext{} + context.Init() + return context.Parse(reader, ignoreCritical) +} + +type BiftGeneralStatusEncoder struct { + Length uint +} + +type BiftGeneralStatusParsingContext struct { +} + +func (encoder *BiftGeneralStatusEncoder) Init(value *BiftGeneralStatus) { + + l := uint(0) + if optval, ok := value.BierIndex.Get(); ok { + l += 3 + l += uint(1 + enc.Nat(optval).EncodingLength()) + } + encoder.Length = l + +} + +func (context *BiftGeneralStatusParsingContext) Init() { + +} + +func (encoder *BiftGeneralStatusEncoder) EncodeInto(value *BiftGeneralStatus, buf []byte) { + + pos := uint(0) + + if optval, ok := value.BierIndex.Get(); ok { + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(774)) + pos += 3 + + buf[pos] = byte(enc.Nat(optval).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + + } +} + +func (encoder *BiftGeneralStatusEncoder) Encode(value *BiftGeneralStatus) enc.Wire { + + wire := make(enc.Wire, 1) + wire[0] = make([]byte, encoder.Length) + buf := wire[0] + encoder.EncodeInto(value, buf) + + return wire +} + +func (context *BiftGeneralStatusParsingContext) Parse(reader enc.WireView, ignoreCritical bool) (*BiftGeneralStatus, error) { + + var handled_BierIndex bool = false + + progress := -1 + _ = progress + + value := &BiftGeneralStatus{} + var err error + var startPos int + for { + startPos = reader.Pos() + if startPos >= reader.Length() { + break + } + typ := enc.TLNum(0) + l := enc.TLNum(0) + typ, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + l, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + + err = nil + if handled := false; true { + switch typ { + case 774: + if true { + handled = true + handled_BierIndex = true + { + optval := uint64(0) + optval = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + optval = uint64(optval<<8) | uint64(x) + } + } + value.BierIndex.Set(optval) + } + } + default: + if !ignoreCritical && ((typ <= 31) || ((typ & 1) == 1)) { + return nil, enc.ErrUnrecognizedField{TypeNum: typ} + } + handled = true + err = reader.Skip(int(l)) + } + if err == nil && !handled { + } + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: typ, Err: err} + } + } + } + + startPos = reader.Pos() + err = nil + + if !handled_BierIndex && err == nil { + value.BierIndex.Unset() + } + + if err != nil { + return nil, err + } + + return value, nil +} + +func (value *BiftGeneralStatus) Encode() enc.Wire { + encoder := BiftGeneralStatusEncoder{} + encoder.Init(value) + return encoder.Encode(value) +} + +func (value *BiftGeneralStatus) Bytes() []byte { + return value.Encode().Join() +} + +func ParseBiftGeneralStatus(reader enc.WireView, ignoreCritical bool) (*BiftGeneralStatus, error) { + context := BiftGeneralStatusParsingContext{} + context.Init() + return context.Parse(reader, ignoreCritical) +} + +type BiftEntryEncoder struct { + Length uint + + RouterName_length uint + NextHops_subencoder []struct { + } + FbmBits_subencoder []struct { + } +} + +type BiftEntryParsingContext struct { +} + +func (encoder *BiftEntryEncoder) Init(value *BiftEntry) { + + if value.RouterName != nil { + encoder.RouterName_length = 0 + for _, c := range value.RouterName { + encoder.RouterName_length += uint(c.EncodingLength()) + } + } + { + NextHops_l := len(value.NextHops) + encoder.NextHops_subencoder = make([]struct { + }, NextHops_l) + for i := 0; i < NextHops_l; i++ { + pseudoEncoder := &encoder.NextHops_subencoder[i] + pseudoValue := struct { + NextHops uint64 + }{ + NextHops: value.NextHops[i], + } + { + encoder := pseudoEncoder + value := &pseudoValue + + _ = encoder + _ = value + } + } + } + { + FbmBits_l := len(value.FbmBits) + encoder.FbmBits_subencoder = make([]struct { + }, FbmBits_l) + for i := 0; i < FbmBits_l; i++ { + pseudoEncoder := &encoder.FbmBits_subencoder[i] + pseudoValue := struct { + FbmBits uint64 + }{ + FbmBits: value.FbmBits[i], + } + { + encoder := pseudoEncoder + value := &pseudoValue + + _ = encoder + _ = value + } + } + } + + l := uint(0) + l += 3 + l += uint(1 + enc.Nat(value.BfrId).EncodingLength()) + if value.RouterName != nil { + l += 1 + l += uint(enc.TLNum(encoder.RouterName_length).EncodingLength()) + l += encoder.RouterName_length + } + if value.NextHops != nil { + for seq_i, seq_v := range value.NextHops { + pseudoEncoder := &encoder.NextHops_subencoder[seq_i] + pseudoValue := struct { + NextHops uint64 + }{ + NextHops: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + l += 1 + l += uint(1 + enc.Nat(value.NextHops).EncodingLength()) + _ = encoder + _ = value + } + } + } + if value.FbmBits != nil { + for seq_i, seq_v := range value.FbmBits { + pseudoEncoder := &encoder.FbmBits_subencoder[seq_i] + pseudoValue := struct { + FbmBits uint64 + }{ + FbmBits: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + l += 3 + l += uint(1 + enc.Nat(value.FbmBits).EncodingLength()) + _ = encoder + _ = value + } + } + } + encoder.Length = l + +} + +func (context *BiftEntryParsingContext) Init() { + +} + +func (encoder *BiftEntryEncoder) EncodeInto(value *BiftEntry, buf []byte) { + + pos := uint(0) + + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(774)) + pos += 3 + + buf[pos] = byte(enc.Nat(value.BfrId).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + if value.RouterName != nil { + buf[pos] = byte(7) + pos += 1 + pos += uint(enc.TLNum(encoder.RouterName_length).EncodeInto(buf[pos:])) + for _, c := range value.RouterName { + pos += uint(c.EncodeInto(buf[pos:])) + } + } + if value.NextHops != nil { + for seq_i, seq_v := range value.NextHops { + pseudoEncoder := &encoder.NextHops_subencoder[seq_i] + pseudoValue := struct { + NextHops uint64 + }{ + NextHops: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + buf[pos] = byte(105) + pos += 1 + + buf[pos] = byte(enc.Nat(value.NextHops).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + _ = encoder + _ = value + } + } + } + if value.FbmBits != nil { + for seq_i, seq_v := range value.FbmBits { + pseudoEncoder := &encoder.FbmBits_subencoder[seq_i] + pseudoValue := struct { + FbmBits uint64 + }{ + FbmBits: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(776)) + pos += 3 + + buf[pos] = byte(enc.Nat(value.FbmBits).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + _ = encoder + _ = value + } + } + } +} + +func (encoder *BiftEntryEncoder) Encode(value *BiftEntry) enc.Wire { + + wire := make(enc.Wire, 1) + wire[0] = make([]byte, encoder.Length) + buf := wire[0] + encoder.EncodeInto(value, buf) + + return wire +} + +func (context *BiftEntryParsingContext) Parse(reader enc.WireView, ignoreCritical bool) (*BiftEntry, error) { + + var handled_BfrId bool = false + var handled_RouterName bool = false + var handled_NextHops bool = false + var handled_FbmBits bool = false + + progress := -1 + _ = progress + + value := &BiftEntry{} + var err error + var startPos int + for { + startPos = reader.Pos() + if startPos >= reader.Length() { + break + } + typ := enc.TLNum(0) + l := enc.TLNum(0) + typ, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + l, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + + err = nil + if handled := false; true { + switch typ { + case 774: + if true { + handled = true + handled_BfrId = true + value.BfrId = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.BfrId = uint64(value.BfrId<<8) | uint64(x) + } + } + } + case 7: + if true { + handled = true + handled_RouterName = true + delegate := reader.Delegate(int(l)) + value.RouterName, err = delegate.ReadName() + } + case 105: + if true { + handled = true + handled_NextHops = true + if value.NextHops == nil { + value.NextHops = make([]uint64, 0) + } + { + pseudoValue := struct { + NextHops uint64 + }{} + { + value := &pseudoValue + value.NextHops = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.NextHops = uint64(value.NextHops<<8) | uint64(x) + } + } + _ = value + } + value.NextHops = append(value.NextHops, pseudoValue.NextHops) + } + progress-- + } + case 776: + if true { + handled = true + handled_FbmBits = true + if value.FbmBits == nil { + value.FbmBits = make([]uint64, 0) + } + { + pseudoValue := struct { + FbmBits uint64 + }{} + { + value := &pseudoValue + value.FbmBits = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.FbmBits = uint64(value.FbmBits<<8) | uint64(x) + } + } + _ = value + } + value.FbmBits = append(value.FbmBits, pseudoValue.FbmBits) + } + progress-- + } + default: + if !ignoreCritical && ((typ <= 31) || ((typ & 1) == 1)) { + return nil, enc.ErrUnrecognizedField{TypeNum: typ} + } + handled = true + err = reader.Skip(int(l)) + } + if err == nil && !handled { + } + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: typ, Err: err} + } + } + } + + startPos = reader.Pos() + err = nil + + if !handled_BfrId && err == nil { + err = enc.ErrSkipRequired{Name: "BfrId", TypeNum: 774} + } + if !handled_RouterName && err == nil { + value.RouterName = nil + } + if !handled_NextHops && err == nil { + // sequence - skip + } + if !handled_FbmBits && err == nil { + // sequence - skip + } + + if err != nil { + return nil, err + } + + return value, nil +} + +func (value *BiftEntry) Encode() enc.Wire { + encoder := BiftEntryEncoder{} + encoder.Init(value) + return encoder.Encode(value) +} + +func (value *BiftEntry) Bytes() []byte { + return value.Encode().Join() +} + +func ParseBiftEntry(reader enc.WireView, ignoreCritical bool) (*BiftEntry, error) { + context := BiftEntryParsingContext{} + context.Init() + return context.Parse(reader, ignoreCritical) +} + +type BiftNeighborEncoder struct { + Length uint + + FbmBits_subencoder []struct { + } +} + +type BiftNeighborParsingContext struct { +} + +func (encoder *BiftNeighborEncoder) Init(value *BiftNeighbor) { + + { + FbmBits_l := len(value.FbmBits) + encoder.FbmBits_subencoder = make([]struct { + }, FbmBits_l) + for i := 0; i < FbmBits_l; i++ { + pseudoEncoder := &encoder.FbmBits_subencoder[i] + pseudoValue := struct { + FbmBits uint64 + }{ + FbmBits: value.FbmBits[i], + } + { + encoder := pseudoEncoder + value := &pseudoValue + + _ = encoder + _ = value + } + } + } + + l := uint(0) + l += 1 + l += uint(1 + enc.Nat(value.FaceID).EncodingLength()) + if value.FbmBits != nil { + for seq_i, seq_v := range value.FbmBits { + pseudoEncoder := &encoder.FbmBits_subencoder[seq_i] + pseudoValue := struct { + FbmBits uint64 + }{ + FbmBits: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + l += 3 + l += uint(1 + enc.Nat(value.FbmBits).EncodingLength()) + _ = encoder + _ = value + } + } + } + encoder.Length = l + +} + +func (context *BiftNeighborParsingContext) Init() { + +} + +func (encoder *BiftNeighborEncoder) EncodeInto(value *BiftNeighbor, buf []byte) { + + pos := uint(0) + + buf[pos] = byte(105) + pos += 1 + + buf[pos] = byte(enc.Nat(value.FaceID).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + if value.FbmBits != nil { + for seq_i, seq_v := range value.FbmBits { + pseudoEncoder := &encoder.FbmBits_subencoder[seq_i] + pseudoValue := struct { + FbmBits uint64 + }{ + FbmBits: seq_v, + } + { + encoder := pseudoEncoder + value := &pseudoValue + buf[pos] = 253 + binary.BigEndian.PutUint16(buf[pos+1:], uint16(776)) + pos += 3 + + buf[pos] = byte(enc.Nat(value.FbmBits).EncodeInto(buf[pos+1:])) + pos += uint(1 + buf[pos]) + _ = encoder + _ = value + } + } + } +} + +func (encoder *BiftNeighborEncoder) Encode(value *BiftNeighbor) enc.Wire { + + wire := make(enc.Wire, 1) + wire[0] = make([]byte, encoder.Length) + buf := wire[0] + encoder.EncodeInto(value, buf) + + return wire +} + +func (context *BiftNeighborParsingContext) Parse(reader enc.WireView, ignoreCritical bool) (*BiftNeighbor, error) { + + var handled_FaceID bool = false + var handled_FbmBits bool = false + + progress := -1 + _ = progress + + value := &BiftNeighbor{} + var err error + var startPos int + for { + startPos = reader.Pos() + if startPos >= reader.Length() { + break + } + typ := enc.TLNum(0) + l := enc.TLNum(0) + typ, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + l, err = reader.ReadTLNum() + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: 0, Err: err} + } + + err = nil + if handled := false; true { + switch typ { + case 105: + if true { + handled = true + handled_FaceID = true + value.FaceID = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.FaceID = uint64(value.FaceID<<8) | uint64(x) + } + } + } + case 776: + if true { + handled = true + handled_FbmBits = true + if value.FbmBits == nil { + value.FbmBits = make([]uint64, 0) + } + { + pseudoValue := struct { + FbmBits uint64 + }{} + { + value := &pseudoValue + value.FbmBits = uint64(0) + { + for i := 0; i < int(l); i++ { + x := byte(0) + x, err = reader.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + value.FbmBits = uint64(value.FbmBits<<8) | uint64(x) + } + } + _ = value + } + value.FbmBits = append(value.FbmBits, pseudoValue.FbmBits) + } + progress-- + } + default: + if !ignoreCritical && ((typ <= 31) || ((typ & 1) == 1)) { + return nil, enc.ErrUnrecognizedField{TypeNum: typ} + } + handled = true + err = reader.Skip(int(l)) + } + if err == nil && !handled { + } + if err != nil { + return nil, enc.ErrFailToParse{TypeNum: typ, Err: err} + } + } + } + + startPos = reader.Pos() + err = nil + + if !handled_FaceID && err == nil { + err = enc.ErrSkipRequired{Name: "FaceID", TypeNum: 105} + } + if !handled_FbmBits && err == nil { + // sequence - skip + } + + if err != nil { + return nil, err + } + + return value, nil +} + +func (value *BiftNeighbor) Encode() enc.Wire { + encoder := BiftNeighborEncoder{} + encoder.Init(value) + return encoder.Encode(value) +} + +func (value *BiftNeighbor) Bytes() []byte { + return value.Encode().Join() +} + +func ParseBiftNeighbor(reader enc.WireView, ignoreCritical bool) (*BiftNeighbor, error) { + context := BiftNeighborParsingContext{} + context.Init() + return context.Parse(reader, ignoreCritical) +} diff --git a/tools/nfdc/ndnd-status.html b/tools/nfdc/ndnd-status.html new file mode 100644 index 00000000..8bf56079 --- /dev/null +++ b/tools/nfdc/ndnd-status.html @@ -0,0 +1,138 @@ + + + + + + NDNd Status + + + +
+
+

NDNd Status

+

Generated

+
+ +
+

General Status

+ + + + + + + + +
Version{{.General.Version}}Start time{{.General.StartTime}}
Current time{{.General.CurrentTime}}Uptime{{.General.Uptime}}
FIB entries{{.General.NFIBEntries}}PIT entries{{.General.NPITEntries}}
CS entries{{.General.NCsEntries}}In Interests{{.General.NInInterests}}
Out Interests{{.General.NOutInterests}}In Data{{.General.NInData}}
Out Data{{.General.NOutData}}Satisfied Interests{{.General.NSatisfiedInterests}}
Unsatisfied Interests{{.General.NUnsatisfiedInterests}}
+
+ +
+

Faces

+
+ + + + + {{range .Faces.Entries}} + + + + + + + + + {{else}}{{end}} +
IDRemote URILocal URIScopePersistencyLink typeMTUBase mark interval (ns)Congestion threshold (bytes)FlagsExpiry (ms)In InterestsIn DataIn BytesOut InterestsOut DataOut Bytes
{{.ID}}{{.RemoteURI}}{{.LocalURI}}{{.Scope}}{{.Persistency}}{{.LinkType}}{{optionalUint .MTU}}{{optionalUint .BaseCongestionMarkInterval}}{{optionalUint .DefaultCongestionThreshold}}{{.Flags}}{{if .ExpirationMilliseconds}}{{optionalUint .ExpirationMilliseconds}}{{else}}not set{{end}}{{.NInInterests}}{{.NInData}}{{.NInBytes}}{{.NOutInterests}}{{.NOutData}}{{.NOutBytes}}
No faces
+
+ +
+

FIB

+
+ {{range .FIB.Entries}} + {{else}}{{end}} +
PrefixNext hops
{{.Prefix}}{{range .NextHops}}face {{.FaceID}} (cost {{.Cost}})
{{end}}
No entries
+
+ +
+

Prefix Egress Table (PET)

+
+ {{range .PET.Entries}} + {{else}}{{end}} +
PrefixMulticastEgress routersDirect next hops
{{.Prefix}}{{.Multicast}}{{range .EgressRouters}}{{.}}
{{else}}None{{end}}
{{range .NextHops}}face {{.FaceID}} (cost {{.Cost}})
{{else}}None{{end}}
No PET entries
+
+ +
+

BIER / Bit Index Forwarding Table (BIFT)

+

{{if .BIFT.Enabled}}BIER enabled; local bit index: {{optionalUint .BIFT.BierIndex}}{{else}}BIER disabled{{end}}

+
+ {{range .BIFT.Entries}} + {{else}}{{end}} +
BFR-IDRouterNext hopsForwarding mask (bit positions)
{{.BfrID}}{{.Router}}{{range .NextHops}}face {{.FaceID}}
{{else}}None{{end}}
{{bitList .FbmBits}}
No BIFT entries
+

Neighbor forwarding masks

+
+ {{range .BIFT.Neighbors}} + {{else}}{{end}} +
FaceForwarding mask (bit positions)
{{.FaceID}}{{bitList .FbmBits}}
No neighbor masks
+
+ +
+

RIB

+
+ {{range .RIB.Entries}}{{ $prefix := .Prefix }}{{range .Routes}}{{end}} + {{else}}{{end}} +
PrefixFaceOriginCostFlagsExpiry (ms)
{{$prefix}}{{.FaceID}}{{.Origin}}{{.Cost}}{{.Flags}}{{optionalUint .ExpirationMillis}}
No entries
+
+ +
+

Content Store

+ + + +
CapacityAdmitServeEntriesHitsMisses
{{.ContentStore.Capacity}}{{.ContentStore.Admit}}{{.ContentStore.Serve}}{{.ContentStore.Entries}}{{.ContentStore.Hits}}{{.ContentStore.Misses}}
+
+ +
+

Strategy Choices

+ + {{range .Strategies.Entries}} + {{else}}{{end}} +
PrefixStrategy
{{.Prefix}}{{.Strategy}}
No entries
+
+ +
+

DV

+ {{if .DV.Available}} + + + + + +
Version{{.DV.Version}}Network{{.DV.NetworkName}}
Router{{.DV.RouterName}}RIB entries{{.DV.NRibEntries}}
PSD entries{{.DV.NPsdEntries}}
Neighbors{{.DV.NNeighbors}}FIB entries{{.DV.NFibEntries}}
+ {{if .DV.DatasetAvailable}} +

Reachable routers and prefix announcements

+
+ {{range .DV.Routers}} + {{else}}{{end}} +
RouterNext hopRouter costAnnounced prefixes
{{.Name}}{{.NextHop}}{{.Cost}}{{range .Prefixes}}{{.Name}}{{if .Multicast}} (multicast){{end}}{{if .NotBefore}} valid from {{.NotBefore}}{{end}}{{if .NotAfter}} until {{.NotAfter}}{{end}}
{{else}}No prefixes{{end}}
No reachable routers
+ {{else}}

DV routing dataset unavailable

{{end}} + {{else}}

DV status unavailable

{{end}} +
+ +
NDNd status generated {{.GeneratedAt}}.
+
+ + diff --git a/tools/nfdc/nfdc.go b/tools/nfdc/nfdc.go index b4dde2fc..fc400558 100644 --- a/tools/nfdc/nfdc.go +++ b/tools/nfdc/nfdc.go @@ -7,6 +7,7 @@ import ( enc "github.com/named-data/ndnd/std/encoding" "github.com/named-data/ndnd/std/engine" "github.com/named-data/ndnd/std/ndn" + sec "github.com/named-data/ndnd/std/security" "github.com/spf13/cobra" ) @@ -24,6 +25,16 @@ func Cmds() []*cobra.Command { Short: "Print general status", Args: cobra.NoArgs, Run: t.ExecStatusGeneral, + }, { + Use: "status-report", + Short: "Print the NDNd status report as XML or JSON", + Args: cobra.NoArgs, + RunE: t.RunStatusReport, + }, { + Use: "status-server", + Short: "Serve the NDNd status report over HTTP", + Args: cobra.NoArgs, + RunE: t.RunStatusServer, }, { Use: "face-list", Short: "Print face table", @@ -115,11 +126,24 @@ func Cmds() []*cobra.Command { Args: cobra.ArbitraryArgs, Run: cmd("strategy-choice", "unset", []string{}), }}...) + for _, command := range cmds { + if command.Use == "status-report" { + command.Flags().StringVar(&t.statusFormat, "format", "xml", "Report format: xml or json") + } + if command.Use == "status-report" || command.Use == "status-server" { + command.Flags().StringVar(&t.statusTrustSchemaPath, "trust-schema", "", "Compiled DV trust schema file (default: bundled DV schema)") + command.Flags().StringArrayVar(&t.statusTrustAnchors, "trust-anchor", nil, "Trusted DV root certificate file (PEM or binary; repeat for multiple files)") + } + } return cmds } type Tool struct { - engine ndn.Engine + engine ndn.Engine + statusTrustAnchors []string + statusTrustSchemaPath string + statusTrust *sec.TrustConfig + statusFormat string } // (AI GENERATED DESCRIPTION): Initializes the tool's engine with a new BasicEngine using a default face if not already set, then starts the engine, exiting the program on startup failure. diff --git a/tools/nfdc/nfdc_bift.go b/tools/nfdc/nfdc_bift.go index 268c655f..9dac75c6 100644 --- a/tools/nfdc/nfdc_bift.go +++ b/tools/nfdc/nfdc_bift.go @@ -1,14 +1,13 @@ package nfdc import ( - "encoding/json" "fmt" "os" "strconv" "strings" - "github.com/named-data/ndnd/fw/bier" enc "github.com/named-data/ndnd/std/encoding" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" "github.com/spf13/cobra" ) @@ -45,26 +44,18 @@ func (t *Tool) ExecBiftList(_ *cobra.Command, _ []string) { t.Start() defer t.Stop() - suffix := enc.Name{ - enc.NewGenericComponent("bift"), - enc.NewGenericComponent("list"), - } - - data, err := t.fetchStatusDataset(suffix) + status, err := t.fetchBiftStatus() if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching status dataset: %+v\n", err) + fmt.Fprintf(os.Stderr, "Error fetching BIFT status: %+v\n", err) os.Exit(1) return } - var status bier.BiftStatus - if err := json.Unmarshal(data.Join(), &status); err != nil { - fmt.Fprintf(os.Stderr, "Error parsing BIFT status: %+v\n", err) - os.Exit(1) - return + if index, enabled := status.General.BierIndex.Get(); enabled { + fmt.Printf("BIFT: bier-index=%d\n", index) + } else { + fmt.Println("BIFT: bier-index=-1 (disabled)") } - - fmt.Printf("BIFT: bier-index=%d\n", status.BierIndex) for _, entry := range status.Entries { fmt.Printf(" index=%d router=%s nexthops={%s} fbm={%s}\n", entry.BfrId, entry.RouterName, formatBiftNextHops(entry.NextHops), formatBitList(entry.FbmBits)) @@ -80,14 +71,14 @@ func (t *Tool) ExecBiftList(_ *cobra.Command, _ []string) { } } -func formatBitList(bits []int) string { +func formatBitList(bits []uint64) string { if len(bits) == 0 { return "" } parts := make([]string, 0, len(bits)) for _, bit := range bits { - parts = append(parts, strconv.Itoa(bit)) + parts = append(parts, strconv.FormatUint(bit, 10)) } return strings.Join(parts, ", ") } @@ -99,3 +90,18 @@ func formatBiftNextHops(nextHops []uint64) string { } return strings.Join(parts, ", ") } + +func (t *Tool) fetchBiftStatus() (*mgmt.BiftStatus, error) { + data, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("bift"), enc.NewGenericComponent("list")}) + if err != nil { + return nil, fmt.Errorf("fetch BIFT: %w", err) + } + status, err := mgmt.ParseBiftStatus(enc.NewWireView(data), false) + if err != nil { + return nil, fmt.Errorf("parse BIFT: %w", err) + } + if status.General == nil { + return nil, fmt.Errorf("BIFT status has no general status payload") + } + return status, nil +} diff --git a/tools/nfdc/nfdc_dataset.go b/tools/nfdc/nfdc_dataset.go index fdc952b2..42c6defc 100644 --- a/tools/nfdc/nfdc_dataset.go +++ b/tools/nfdc/nfdc_dataset.go @@ -1,29 +1,60 @@ package nfdc import ( + "fmt" + "time" + enc "github.com/named-data/ndnd/std/encoding" "github.com/named-data/ndnd/std/ndn" "github.com/named-data/ndnd/std/object" + sec "github.com/named-data/ndnd/std/security" + "github.com/named-data/ndnd/std/types/optional" + "github.com/named-data/ndnd/std/utils" ) -// (AI GENERATED DESCRIPTION): Fetches and returns the raw wire representation of a Data packet identified by the supplied suffix, using a consume‑only client to retrieve the content or report an error. func (t *Tool) fetchStatusDataset(suffix enc.Name) (enc.Wire, error) { - // consume-only client, no need for a store - client := object.NewClient(t.engine, nil, nil) - client.Start() - defer client.Stop() + // NFD management datasets have no RDR metadata. + return t.fetchObjectDataset(t.Prefix().Append(suffix...), true, nil) +} - ch := make(chan ndn.ConsumeState) +func (t *Tool) fetchDataPacket(name enc.Name) (ndn.Data, error) { + interest, err := t.engine.Spec().MakeInterest(name, &ndn.InterestConfig{ + MustBeFresh: true, + Lifetime: optional.Some(time.Second), + Nonce: utils.ConvertNonce(t.engine.Timer().Nonce()), + }, nil, nil) + if err != nil { + return nil, err + } + + resultCh := make(chan ndn.ExpressCallbackArgs, 1) + if err := t.engine.Express(interest, func(args ndn.ExpressCallbackArgs) { + resultCh <- args + }); err != nil { + return nil, err + } + + result := <-resultCh + if result.Result != ndn.InterestResultData { + if result.Error != nil { + return nil, result.Error + } + return nil, fmt.Errorf("Interest completed with result %s", result.Result) + } + return result.Data, nil +} + +func (t *Tool) fetchObjectDataset(name enc.Name, noMetadata bool, trust *sec.TrustConfig) (enc.Wire, error) { + // This client only consumes; it does not need to attach a producer handler. + client := object.NewClient(t.engine, nil, trust) + ch := make(chan ndn.ConsumeState, 1) client.ConsumeExt(ndn.ConsumeExtArgs{ - Name: t.Prefix().Append(suffix...), - NoMetadata: true, // NFD has no RDR metadata - Callback: func(status ndn.ConsumeState) { ch <- status }, + Name: name, NoMetadata: noMetadata, + Callback: func(state ndn.ConsumeState) { ch <- state }, }) - state := <-ch if err := state.Error(); err != nil { return nil, err } - return state.Content(), nil } diff --git a/tools/nfdc/nfdc_pet.go b/tools/nfdc/nfdc_pet.go index 371106d0..490f8cff 100644 --- a/tools/nfdc/nfdc_pet.go +++ b/tools/nfdc/nfdc_pet.go @@ -15,21 +15,9 @@ func (t *Tool) ExecPetList(_ *cobra.Command, _ []string) { t.Start() defer t.Stop() - suffix := enc.Name{ - enc.NewGenericComponent("pet"), - enc.NewGenericComponent("list"), - } - - data, err := t.fetchStatusDataset(suffix) - if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching status dataset: %+v\n", err) - os.Exit(1) - return - } - - status, err := mgmt.ParsePetStatus(enc.NewWireView(data), true) + status, err := t.fetchPetStatus() if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing PET status: %+v\n", err) + fmt.Fprintf(os.Stderr, "Error fetching PET status: %+v\n", err) os.Exit(1) return } @@ -55,3 +43,15 @@ func (t *Tool) ExecPetList(_ *cobra.Command, _ []string) { entry.Name, strings.Join(egressList, ", "), strings.Join(nexthops, ", "), multicastStr) } } + +func (t *Tool) fetchPetStatus() (*mgmt.PetStatus, error) { + data, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("pet"), enc.NewGenericComponent("list")}) + if err != nil { + return nil, fmt.Errorf("fetch PET: %w", err) + } + status, err := mgmt.ParsePetStatus(enc.NewWireView(data), true) + if err != nil { + return nil, fmt.Errorf("parse PET: %w", err) + } + return status, nil +} diff --git a/tools/nfdc/nfdc_status_dv2.go b/tools/nfdc/nfdc_status_dv2.go new file mode 100644 index 00000000..38d180c4 --- /dev/null +++ b/tools/nfdc/nfdc_status_dv2.go @@ -0,0 +1,73 @@ +package nfdc + +type ndndPET struct { + Entries []ndndPETEntry `xml:"entry" json:"entries"` +} + +type ndndPETEntry struct { + Prefix string `xml:"prefix,attr" json:"prefix"` + Multicast bool `xml:"multicast,attr" json:"multicast"` + EgressRouters []string `xml:"egress>router" json:"egressRouters"` + NextHops []ndndNextHop `xml:"nextHop" json:"nextHop"` +} + +type ndndBIFT struct { + Enabled bool `xml:"enabled,attr" json:"enabled"` + BierIndex *uint64 `xml:"bierIndex,omitempty" json:"bierIndex,omitempty"` + Entries []ndndBIFTEntry `xml:"entries>entry" json:"entries"` + Neighbors []ndndBIFTNeighbor `xml:"neighbors>neighbor" json:"neighbors"` +} + +type ndndBIFTEntry struct { + BfrID uint64 `xml:"bfrId,attr" json:"bfrId"` + Router string `xml:"router" json:"router"` + NextHops []ndndBIERNextHop `xml:"nextHop" json:"nextHop"` + FbmBits []uint64 `xml:"fbmBits>bit" json:"fbmBits"` +} + +type ndndBIERNextHop struct { + FaceID uint64 `xml:"faceId,attr" json:"faceId,string"` +} + +type ndndBIFTNeighbor struct { + FaceID uint64 `xml:"faceId,attr" json:"faceId,string"` + FbmBits []uint64 `xml:"fbmBits>bit" json:"fbmBits"` +} + +func (t *Tool) populatePET(report *ndndStatusReport) error { + status, err := t.fetchPetStatus() + if err != nil { + return err + } + for _, entry := range status.Entries { + item := ndndPETEntry{Prefix: entry.Name.String(), Multicast: entry.Multicast} + for _, egress := range entry.EgressRecords { + item.EgressRouters = append(item.EgressRouters, egress.Name.String()) + } + item.NextHops = ndndNextHops(entry.NextHopRecords) + report.PET.Entries = append(report.PET.Entries, item) + } + return nil +} + +func (t *Tool) populateBIFT(report *ndndStatusReport) error { + status, err := t.fetchBiftStatus() + if err != nil { + return err + } + report.BIFT = ndndBIFT{Enabled: status.General.BierIndex.IsSet()} + if index, enabled := status.General.BierIndex.Get(); enabled { + report.BIFT.BierIndex = &index + } + for _, entry := range status.Entries { + item := ndndBIFTEntry{BfrID: entry.BfrId, Router: entry.RouterName.String(), FbmBits: entry.FbmBits} + for _, face := range entry.NextHops { + item.NextHops = append(item.NextHops, ndndBIERNextHop{FaceID: face}) + } + report.BIFT.Entries = append(report.BIFT.Entries, item) + } + for _, neighbor := range status.Neighbors { + report.BIFT.Neighbors = append(report.BIFT.Neighbors, ndndBIFTNeighbor{FaceID: neighbor.FaceID, FbmBits: neighbor.FbmBits}) + } + return nil +} diff --git a/tools/nfdc/nfdc_status_dv2_test.go b/tools/nfdc/nfdc_status_dv2_test.go new file mode 100644 index 00000000..44ecb8c0 --- /dev/null +++ b/tools/nfdc/nfdc_status_dv2_test.go @@ -0,0 +1,164 @@ +package nfdc + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/engine" + "github.com/named-data/ndnd/std/engine/face" + "github.com/named-data/ndnd/std/log" + "github.com/named-data/ndnd/std/ndn" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" + "github.com/named-data/ndnd/std/object" + "github.com/named-data/ndnd/std/object/storage" + "github.com/named-data/ndnd/std/types/optional" + "github.com/stretchr/testify/require" +) + +type dv2DatasetTestEngine struct { + ndn.Engine + store ndn.Store + queue chan func() +} + +func (e *dv2DatasetTestEngine) Post(fn func()) { e.queue <- fn } + +func (e *dv2DatasetTestEngine) Express(packet *ndn.EncodedInterest, callback ndn.ExpressCallbackFunc) error { + interest, _, err := e.Spec().ReadInterest(enc.NewWireView(packet.Wire)) + if err != nil { + return err + } + e.Post(func() { + wire, err := e.store.Get(interest.Name(), interest.CanBePrefix()) + if err != nil || wire == nil { + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultError, Error: fmt.Errorf("test dataset unavailable")}) + return + } + data, covered, err := e.Spec().ReadData(enc.NewBufferView(wire)) + if err != nil { + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultError, Error: err}) + return + } + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultData, Data: data, SigCovered: covered, RawData: enc.Wire{wire}}) + }) + return nil +} + +func dv2DatasetTestTool(t *testing.T, datasets map[string]enc.Wire) *Tool { + t.Helper() + transport := &dv2DatasetTestEngine{Engine: engine.NewBasicEngine(face.NewDummyFace()), store: storage.NewMemoryStore(), queue: make(chan func(), 512)} + for suffix, content := range datasets { + _, err := object.Produce(ndn.ProduceArgs{Name: statusTrustName(t, "/localhost/nfd/"+suffix).WithVersion(1), Content: content, NoMetadata: true}, transport.store, nil) + require.NoError(t, err) + } + stop, stopped := make(chan struct{}), make(chan struct{}) + go func() { + defer close(stopped) + for { + select { + case fn := <-transport.queue: + fn() + case <-stop: + return + } + } + }() + t.Cleanup(func() { close(stop); <-stopped }) + return &Tool{engine: transport} +} + +func TestDV2StatusDatasetsAndReports(t *testing.T) { + large := ^uint64(0) + pet := &mgmt.PetStatus{Entries: []*mgmt.PetEntry{{Name: statusTrustName(t, "/app/data"), Multicast: true, + EgressRecords: []*mgmt.EgressRecord{{Name: statusTrustName(t, "/example/a")}}, + NextHopRecords: []*mgmt.NextHopRecord{{FaceId: large, Cost: large}}, + }}} + bift := &mgmt.BiftStatus{General: &mgmt.BiftGeneralStatus{BierIndex: optional.Some(uint64(0))}, + Entries: []*mgmt.BiftEntry{{BfrId: 5, RouterName: statusTrustName(t, "/example/a"), NextHops: []uint64{large}, FbmBits: []uint64{0, 5}}}, + Neighbors: []*mgmt.BiftNeighbor{{FaceID: large, FbmBits: []uint64{0, 5}}}, + } + fib := &mgmt.FibStatus{Entries: []*mgmt.FibEntry{{Name: pet.Entries[0].Name, NextHopRecords: pet.Entries[0].NextHopRecords}}} + tool := dv2DatasetTestTool(t, map[string]enc.Wire{"fib/list": fib.Encode(), "pet/list": pet.Encode(), "bift/list": bift.Encode()}) + report := &ndndStatusReport{} + require.NoError(t, tool.populateFIB(report)) + require.NoError(t, tool.populatePET(report)) + require.NoError(t, tool.populateBIFT(report)) + require.Equal(t, ndndPET{Entries: []ndndPETEntry{{Prefix: "/app/data", Multicast: true, EgressRouters: []string{"/example/a"}, NextHops: []ndndNextHop{{FaceID: large, Cost: large}}}}}, report.PET) + require.Equal(t, report.PET.Entries[0].NextHops, report.FIB.Entries[0].NextHops) + require.True(t, report.BIFT.Enabled) + require.Equal(t, uint64(0), *report.BIFT.BierIndex) + require.Equal(t, uint64(5), report.BIFT.Entries[0].BfrID) + require.Equal(t, "/example/a", report.BIFT.Entries[0].Router) + require.Equal(t, []ndndBIERNextHop{{FaceID: large}}, report.BIFT.Entries[0].NextHops) + require.Equal(t, []uint64{0, 5}, report.BIFT.Neighbors[0].FbmBits) + handler := statusServerHandler(func() (*ndndStatusReport, error) { return report, nil }, log.Default()) + for _, format := range []string{"xml", "json"} { + t.Run(format, func(t *testing.T) { + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status."+format, nil)) + require.Equal(t, http.StatusOK, response.Code) + var parsed ndndStatusReport + if format == "xml" { + require.NoError(t, xml.Unmarshal(response.Body.Bytes(), &parsed)) + } else { + require.NoError(t, json.Unmarshal(response.Body.Bytes(), &parsed)) + require.Equal(t, 4, strings.Count(response.Body.String(), `"faceId": "18446744073709551615"`)) + require.Contains(t, response.Body.String(), `"cost": "18446744073709551615"`) + } + require.Equal(t, report.PET, parsed.PET) + require.Equal(t, report.BIFT, parsed.BIFT) + }) + } + page, err := renderNDNdStatusHTML(report) + require.NoError(t, err) + for _, text := range []string{"Prefix Egress Table (PET)", "/app/data", "BIER enabled; local bit index: 0", "Neighbor forwarding masks", "0, 5"} { + require.Contains(t, string(page), text) + } +} + +func TestBIFTDisabled(t *testing.T) { + tool := dv2DatasetTestTool(t, map[string]enc.Wire{"bift/list": (&mgmt.BiftStatus{General: &mgmt.BiftGeneralStatus{}}).Encode()}) + report := &ndndStatusReport{} + require.NoError(t, tool.populateBIFT(report)) + require.False(t, report.BIFT.Enabled) + require.Nil(t, report.BIFT.BierIndex) + page, err := renderNDNdStatusHTML(report) + require.NoError(t, err) + require.Contains(t, string(page), "BIER disabled") + require.NotContains(t, string(page), "local bit index:") +} + +func TestBIFTRejectsInvalidPayloads(t *testing.T) { + for _, content := range []enc.Wire{{[]byte(`{"bier_index":0}`)}, nil, (&mgmt.BiftStatus{Entries: []*mgmt.BiftEntry{{BfrId: 1}}}).Encode(), {[]byte{0xfd, 0x03, 0x00, 0x02, 0x00}}} { + tool := dv2DatasetTestTool(t, map[string]enc.Wire{"bift/list": content}) + report := &ndndStatusReport{} + require.Error(t, tool.populateBIFT(report)) + require.False(t, report.BIFT.Enabled) + } +} + +func TestDV2StatusSegmentedDatasets(t *testing.T) { + pet := &mgmt.PetStatus{} + bift := &mgmt.BiftStatus{General: &mgmt.BiftGeneralStatus{}} + for i := range 1000 { + name := statusTrustName(t, fmt.Sprintf("/example/router-%04d", i)) + pet.Entries = append(pet.Entries, &mgmt.PetEntry{Name: name, EgressRecords: []*mgmt.EgressRecord{{Name: name}}}) + bift.Entries = append(bift.Entries, &mgmt.BiftEntry{BfrId: uint64(i), RouterName: name, NextHops: []uint64{7}, FbmBits: []uint64{uint64(i)}}) + } + require.Greater(t, pet.Encode().Length(), uint64(8000)) + require.Greater(t, bift.Encode().Length(), uint64(8000)) + tool := dv2DatasetTestTool(t, map[string]enc.Wire{"pet/list": pet.Encode(), "bift/list": bift.Encode()}) + report := &ndndStatusReport{} + require.NoError(t, tool.populatePET(report)) + require.NoError(t, tool.populateBIFT(report)) + require.Len(t, report.PET.Entries, 1000) + require.Len(t, report.BIFT.Entries, 1000) + require.Equal(t, "/example/router-0999", report.PET.Entries[999].Prefix) + require.Equal(t, uint64(999), report.BIFT.Entries[999].BfrID) +} diff --git a/tools/nfdc/nfdc_status_report.go b/tools/nfdc/nfdc_status_report.go new file mode 100644 index 00000000..302a592c --- /dev/null +++ b/tools/nfdc/nfdc_status_report.go @@ -0,0 +1,532 @@ +package nfdc + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "os" + "strings" + "time" + + dvconfig "github.com/named-data/ndnd/dv/config" + dvtlv "github.com/named-data/ndnd/dv/tlv" + enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/log" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" + "github.com/spf13/cobra" +) + +type ndndStatusReport struct { + XMLName xml.Name `xml:"ndndStatus" json:"-"` + GeneratedAt string `xml:"generatedAt,attr" json:"generatedAt"` + General ndndGeneralStatus `xml:"general" json:"general"` + Faces ndndFaces `xml:"faces" json:"faces"` + FIB ndndFIB `xml:"fib" json:"fib"` + RIB ndndRIB `xml:"rib" json:"rib"` + PET ndndPET `xml:"pet" json:"pet"` + BIFT ndndBIFT `xml:"bift" json:"bift"` + ContentStore ndndContentStore `xml:"contentStore" json:"contentStore"` + Strategies ndndStrategies `xml:"strategies" json:"strategies"` + DV ndndDVStatus `xml:"dv" json:"dv"` +} + +type ndndGeneralStatus struct { + Version string `xml:"version" json:"version"` + StartTime string `xml:"startTime" json:"startTime"` + CurrentTime string `xml:"currentTime" json:"currentTime"` + Uptime string `xml:"uptime" json:"uptime"` + NFIBEntries uint64 `xml:"nFibEntries" json:"nFibEntries"` + NPITEntries uint64 `xml:"nPitEntries" json:"nPitEntries"` + NCsEntries uint64 `xml:"nCsEntries" json:"nCsEntries"` + NInInterests uint64 `xml:"nInInterests" json:"nInInterests,string"` + NOutInterests uint64 `xml:"nOutInterests" json:"nOutInterests,string"` + NInData uint64 `xml:"nInData" json:"nInData,string"` + NOutData uint64 `xml:"nOutData" json:"nOutData,string"` + NSatisfiedInterests uint64 `xml:"nSatisfiedInterests" json:"nSatisfiedInterests,string"` + NUnsatisfiedInterests uint64 `xml:"nUnsatisfiedInterests" json:"nUnsatisfiedInterests,string"` +} + +type ndndFace struct { + ID uint64 `xml:"faceId,attr" json:"faceId,string"` + RemoteURI string `xml:"remoteUri" json:"remoteUri"` + LocalURI string `xml:"localUri" json:"localUri"` + Scope string `xml:"scope" json:"scope"` + Persistency string `xml:"persistency" json:"persistency"` + LinkType string `xml:"linkType" json:"linkType"` + MTU *uint64 `xml:"mtu,omitempty" json:"mtu,omitempty"` + BaseCongestionMarkInterval *uint64 `xml:"baseCongestionMarkInterval,omitempty" json:"baseCongestionMarkInterval,omitempty,string"` + DefaultCongestionThreshold *uint64 `xml:"defaultCongestionThreshold,omitempty" json:"defaultCongestionThreshold,omitempty,string"` + Flags string `xml:"flags" json:"flags"` + ExpirationMilliseconds *uint64 `xml:"expirationMilliseconds,omitempty" json:"expirationMilliseconds,omitempty"` + NInInterests uint64 `xml:"nInInterests" json:"nInInterests,string"` + NInData uint64 `xml:"nInData" json:"nInData,string"` + NInBytes uint64 `xml:"nInBytes" json:"nInBytes,string"` + NOutInterests uint64 `xml:"nOutInterests" json:"nOutInterests,string"` + NOutData uint64 `xml:"nOutData" json:"nOutData,string"` + NOutBytes uint64 `xml:"nOutBytes" json:"nOutBytes,string"` +} + +type ndndDVStatus struct { + Available bool `xml:"available,attr" json:"available"` + Version string `xml:"version" json:"version"` + NetworkName string `xml:"networkName" json:"networkName"` + RouterName string `xml:"routerName" json:"routerName"` + NRibEntries uint64 `xml:"nRibEntries" json:"nRibEntries"` + NNeighbors uint64 `xml:"nNeighbors" json:"nNeighbors"` + NFibEntries uint64 `xml:"nFibEntries" json:"nFibEntries"` + NPsdEntries uint64 `xml:"nPsdEntries" json:"nPsdEntries"` + DatasetAvailable bool `xml:"datasetAvailable,attr" json:"datasetAvailable"` + Routers []ndndDVRouter `xml:"routers>router" json:"routers"` +} + +type ndndDVRouter struct { + Name string `xml:"name,attr" json:"name"` + NextHop string `xml:"nextHop" json:"nextHop"` + Cost uint64 `xml:"cost" json:"cost"` + Prefixes []ndndDVPrefix `xml:"prefixes>prefix" json:"prefixes"` +} + +type ndndDVPrefix struct { + Name string `xml:"name,attr" json:"name"` + Multicast bool `xml:"multicast,attr" json:"multicast"` + NotBefore string `xml:"notBefore,attr,omitempty" json:"notBefore,omitempty"` + NotAfter string `xml:"notAfter,attr,omitempty" json:"notAfter,omitempty"` +} + +type ndndFaces struct { + Entries []ndndFace `xml:"face" json:"entries"` +} + +type ndndFIBEntry struct { + Prefix string `xml:"prefix,attr" json:"prefix"` + NextHops []ndndNextHop `xml:"nextHop" json:"nextHop"` +} + +type ndndFIB struct { + Entries []ndndFIBEntry `xml:"entry" json:"entries"` +} + +type ndndRIBEntry struct { + Prefix string `xml:"prefix,attr" json:"prefix"` + Routes []ndndRoute `xml:"route" json:"route"` +} + +type ndndRIB struct { + Entries []ndndRIBEntry `xml:"entry" json:"entries"` +} + +type ndndNextHop struct { + FaceID uint64 `xml:"faceId,attr" json:"faceId,string"` + Cost uint64 `xml:"cost,attr" json:"cost,string"` +} + +type ndndRoute struct { + FaceID uint64 `xml:"faceId,attr" json:"faceId,string"` + Origin string `xml:"origin,attr" json:"origin"` + Cost uint64 `xml:"cost,attr" json:"cost,string"` + Flags string `xml:"flags,attr" json:"flags"` + ExpirationMillis *uint64 `xml:"expirationMilliseconds,attr,omitempty" json:"expirationMilliseconds,omitempty"` +} + +type ndndContentStore struct { + Capacity uint64 `xml:"capacity,attr" json:"capacity"` + Admit bool `xml:"admit,attr" json:"admit"` + Serve bool `xml:"serve,attr" json:"serve"` + Entries uint64 `xml:"entries,attr" json:"entries"` + Hits uint64 `xml:"hits,attr" json:"hits,string"` + Misses uint64 `xml:"misses,attr" json:"misses,string"` +} + +type ndndStrategy struct { + Prefix string `xml:"prefix,attr" json:"prefix"` + Strategy string `xml:"strategy,attr" json:"strategy"` +} + +type ndndStrategies struct { + Entries []ndndStrategy `xml:"strategy" json:"entries"` +} + +func (t *Tool) RunStatusReport(_ *cobra.Command, _ []string) error { + encode := encodeNdndStatusXML + switch t.statusFormat { + case "xml": + case "json": + encode = encodeNdndStatusJSON + default: + return fmt.Errorf("unsupported status format %q: use xml or json", t.statusFormat) + } + if err := t.configureStatusTrust(); err != nil { + return err + } + t.Start() + defer t.Stop() + + report, err := t.buildNdndStatusReport() + if err != nil { + return fmt.Errorf("unable to build NDNd status report: %w", err) + } + encoded, err := encode(report) + if err != nil { + return fmt.Errorf("unable to encode NDNd status report: %w", err) + } + if _, err := os.Stdout.Write(encoded); err != nil { + return fmt.Errorf("unable to write NDNd status report: %w", err) + } + return nil +} + +func encodeNdndStatusXML(report *ndndStatusReport) ([]byte, error) { + encoded, err := xml.MarshalIndent(report, "", " ") + if err != nil { + return nil, err + } + return append([]byte(xml.Header), encoded...), nil +} + +func encodeNdndStatusJSON(report *ndndStatusReport) ([]byte, error) { + return json.MarshalIndent(report, "", " ") +} + +func (t *Tool) buildNdndStatusReport() (*ndndStatusReport, error) { + generalData, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("status"), enc.NewGenericComponent("general")}) + if err != nil { + return nil, fmt.Errorf("fetch general status: %w", err) + } + general, err := mgmt.ParseGeneralStatus(enc.NewWireView(generalData), true) + if err != nil { + return nil, fmt.Errorf("parse general status: %w", err) + } + + report := &ndndStatusReport{ + GeneratedAt: time.Now().UTC().Format(time.RFC3339), + General: ndndGeneralStatus{ + Version: general.NfdVersion, + StartTime: time.Unix(0, int64(general.StartTimestamp)).UTC().Format(time.RFC3339), + CurrentTime: time.Unix(0, int64(general.CurrentTimestamp)).UTC().Format(time.RFC3339), + Uptime: (general.CurrentTimestamp - general.StartTimestamp).String(), + NFIBEntries: general.NFibEntries, + NPITEntries: general.NPitEntries, + NCsEntries: general.NCsEntries, + NInInterests: general.NInInterests, + NOutInterests: general.NOutInterests, + NInData: general.NInData, + NOutData: general.NOutData, + NSatisfiedInterests: general.NSatisfiedInterests, + NUnsatisfiedInterests: general.NUnsatisfiedInterests, + }, + } + + if err := t.populateFaces(report); err != nil { + return nil, err + } + if err := t.populateFIB(report); err != nil { + return nil, err + } + if err := t.populateRIB(report); err != nil { + return nil, err + } + if err := t.populatePET(report); err != nil { + return nil, err + } + if err := t.populateBIFT(report); err != nil { + return nil, err + } + if err := t.populateContentStore(report); err != nil { + return nil, err + } + if err := t.populateStrategies(report); err != nil { + return nil, err + } + // DV is optional; its errors are logged without discarding forwarder status. + _ = t.populateDVStatus(report) + return report, nil +} + +func (t *Tool) populateFaces(report *ndndStatusReport) error { + data, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("faces"), enc.NewGenericComponent("list")}) + if err != nil { + return fmt.Errorf("fetch faces: %w", err) + } + status, err := mgmt.ParseFaceStatusMsg(enc.NewWireView(data), true) + if err != nil { + return fmt.Errorf("parse faces: %w", err) + } + for _, item := range status.Vals { + face := ndndFace{ + ID: item.FaceId, + RemoteURI: item.Uri, + LocalURI: item.LocalUri, + Scope: faceScopeName(item.FaceScope), + Persistency: mgmt.Persistency(item.FacePersistency).String(), + LinkType: faceLinkTypeName(item.LinkType), + Flags: faceFlagsName(item.Flags), + NInInterests: item.NInInterests, + NInData: item.NInData, + NInBytes: item.NInBytes, + NOutInterests: item.NOutInterests, + NOutData: item.NOutData, + NOutBytes: item.NOutBytes, + } + if interval, ok := item.BaseCongestionMarkInterval.Get(); ok { + face.BaseCongestionMarkInterval = &interval + } + if threshold, ok := item.DefaultCongestionThreshold.Get(); ok { + face.DefaultCongestionThreshold = &threshold + } + if mtu, ok := item.Mtu.Get(); ok { + face.MTU = &mtu + } + if expiration, ok := item.ExpirationPeriod.Get(); ok { + face.ExpirationMilliseconds = &expiration + } + report.Faces.Entries = append(report.Faces.Entries, face) + } + return nil +} + +func (t *Tool) populateFIB(report *ndndStatusReport) error { + data, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("fib"), enc.NewGenericComponent("list")}) + if err != nil { + return fmt.Errorf("fetch FIB: %w", err) + } + status, err := mgmt.ParseFibStatus(enc.NewWireView(data), true) + if err != nil { + return fmt.Errorf("parse FIB: %w", err) + } + for _, item := range status.Entries { + entry := ndndFIBEntry{Prefix: item.Name.String(), NextHops: ndndNextHops(item.NextHopRecords)} + report.FIB.Entries = append(report.FIB.Entries, entry) + } + return nil +} + +func ndndNextHops(records []*mgmt.NextHopRecord) []ndndNextHop { + var hops []ndndNextHop + for _, record := range records { + hops = append(hops, ndndNextHop{FaceID: record.FaceId, Cost: record.Cost}) + } + return hops +} + +func (t *Tool) populateRIB(report *ndndStatusReport) error { + data, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("rib"), enc.NewGenericComponent("list")}) + if err != nil { + return fmt.Errorf("fetch RIB: %w", err) + } + status, err := mgmt.ParseRibStatus(enc.NewWireView(data), true) + if err != nil { + return fmt.Errorf("parse RIB: %w", err) + } + for _, item := range status.Entries { + entry := ndndRIBEntry{Prefix: item.Name.String()} + for _, route := range item.Routes { + flags := make([]string, 0) + for flag, name := range mgmt.RouteFlagList { + if flag.IsSet(route.Flags) { + flags = append(flags, name) + } + } + statusRoute := ndndRoute{ + FaceID: route.FaceId, + Origin: mgmt.RouteOrigin(route.Origin).String(), + Cost: route.Cost, + Flags: strings.Join(flags, ", "), + } + if expiration, ok := route.ExpirationPeriod.Get(); ok { + statusRoute.ExpirationMillis = &expiration + } + entry.Routes = append(entry.Routes, statusRoute) + } + report.RIB.Entries = append(report.RIB.Entries, entry) + } + return nil +} +func (t *Tool) populateContentStore(report *ndndStatusReport) error { + data, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("cs"), enc.NewGenericComponent("info")}) + if err != nil { + return fmt.Errorf("fetch content store: %w", err) + } + status, err := mgmt.ParseCsInfoMsg(enc.NewWireView(data), true) + if err != nil { + return fmt.Errorf("parse content store: %w", err) + } + report.ContentStore, err = ndndContentStoreFromStatus(status) + if err != nil { + return err + } + return nil +} + +func ndndContentStoreFromStatus(status *mgmt.CsInfoMsg) (ndndContentStore, error) { + if status == nil || status.CsInfo == nil { + return ndndContentStore{}, errors.New("content-store status has no CsInfo payload") + } + info := status.CsInfo + return ndndContentStore{ + Capacity: info.Capacity, + Admit: info.Flags&mgmt.CsEnableAdmit != 0, + Serve: info.Flags&mgmt.CsEnableServe != 0, + Entries: info.NCsEntries, + Hits: info.NHits, + Misses: info.NMisses, + }, nil +} + +func (t *Tool) populateStrategies(report *ndndStatusReport) error { + data, err := t.fetchStatusDataset(enc.Name{enc.NewGenericComponent("strategy-choice"), enc.NewGenericComponent("list")}) + if err != nil { + return fmt.Errorf("fetch strategies: %w", err) + } + status, err := mgmt.ParseStrategyChoiceMsg(enc.NewWireView(data), true) + if err != nil { + return fmt.Errorf("parse strategies: %w", err) + } + for _, item := range status.StrategyChoices { + if item.Strategy != nil { + report.Strategies.Entries = append(report.Strategies.Entries, ndndStrategy{Prefix: item.Name.String(), Strategy: item.Strategy.Name.String()}) + } + } + return nil +} + +func (t *Tool) populateDVStatus(report *ndndStatusReport) (err error) { + defer func() { + if err != nil && t.statusTrust != nil { + log.Warn(nil, "DV routing dataset unavailable", "router", report.DV.RouterName, "err", err) + } + }() + data, err := t.fetchDataPacket(dvStatusName()) + if err != nil { + return fmt.Errorf("fetch local DV summary: %w", err) + } + + status, err := ndndDVStatusFromWire(data.Content()) + if err != nil { + return fmt.Errorf("parse local DV summary: %w", err) + } + report.DV = status + if t.statusTrust == nil { + return nil + } + router, err := enc.NameFromStr(status.RouterName) + if err != nil { + return fmt.Errorf("parse DV router name %q: %w", status.RouterName, err) + } + if len(router) == 0 { + return fmt.Errorf("local DV summary contains an empty router name") + } + wire, err := t.fetchObjectDataset(dvconfig.StatusDatasetPrefix(router), false, t.statusTrust) + if err != nil { + return fmt.Errorf("fetch or validate DV routing dataset: %w", err) + } + status, err = ndndDVStatusFromDataset(wire, router) + if err != nil { + return fmt.Errorf("parse DV routing dataset: %w", err) + } + report.DV = status + return nil +} + +func ndndDVStatusFromDataset(wire enc.Wire, router enc.Name) (ndndDVStatus, error) { + dataset, err := dvtlv.ParseRoutingStatus(enc.NewWireView(wire), false) + if err != nil { + return ndndDVStatus{}, err + } + if dataset.Status == nil || dataset.Status.RouterName == nil || !dataset.Status.RouterName.Name.Equal(router) { + return ndndDVStatus{}, fmt.Errorf("routing dataset does not identify the requested router") + } + status := ndndDVStatusFromStatus(dataset.Status) + status.DatasetAvailable = true + for _, item := range dataset.Routers { + if item.Cost >= dvconfig.CostInfinity { + return ndndDVStatus{}, fmt.Errorf("router cost %d for %s must be less than %d", item.Cost, item.Name, dvconfig.CostInfinity) + } + route := ndndDVRouter{Name: item.Name.String(), Cost: item.Cost} + if item.NextHop != nil { + route.NextHop = item.NextHop.Name.String() + } + for _, prefix := range item.Prefixes { + item := ndndDVPrefix{Name: prefix.Name.String(), Multicast: prefix.Multicast} + if prefix.ValidityPeriod != nil { + item.NotBefore = prefix.ValidityPeriod.NotBefore + item.NotAfter = prefix.ValidityPeriod.NotAfter + } + route.Prefixes = append(route.Prefixes, item) + } + status.Routers = append(status.Routers, route) + } + return status, nil +} + +func dvStatusName() enc.Name { + return enc.Name{ + enc.LOCALHOST, + enc.NewGenericComponent("dv"), + enc.NewGenericComponent("status"), + } +} + +func ndndDVStatusFromWire(data enc.Wire) (ndndDVStatus, error) { + status, err := dvtlv.ParseStatus(enc.NewWireView(data), true) + if err != nil { + return ndndDVStatus{}, err + } + return ndndDVStatusFromStatus(status), nil +} + +func ndndDVStatusFromStatus(status *dvtlv.Status) ndndDVStatus { + dvStatus := ndndDVStatus{ + Available: true, + Version: status.Version, + NRibEntries: status.NRibEntries, + NNeighbors: status.NNeighbors, + NFibEntries: status.NFibEntries, + NPsdEntries: status.NPsdEntries, + } + if status.NetworkName != nil { + dvStatus.NetworkName = status.NetworkName.Name.String() + } + if status.RouterName != nil { + dvStatus.RouterName = status.RouterName.Name.String() + } + return dvStatus +} + +func faceScopeName(value uint64) string { + switch value { + case mgmt.FaceScopeLocal: + return "local" + case mgmt.FaceScopeNonLocal: + return "non-local" + default: + return "unknown" + } +} + +func faceLinkTypeName(value uint64) string { + switch value { + case mgmt.FaceLinkPointToPoint: + return "point-to-point" + case mgmt.FaceLinkMultiAccess: + return "multi-access" + case mgmt.FaceLinkAdHoc: + return "ad-hoc" + default: + return "unknown" + } +} + +func faceFlagsName(value uint64) string { + flags := make([]string, 0, 3) + if value&mgmt.FaceFlagLocalFieldsEnabled != 0 { + flags = append(flags, "local-fields") + } + if value&mgmt.FaceFlagLpReliabilityEnabled != 0 { + flags = append(flags, "lp-reliability") + } + if value&mgmt.FaceFlagCongestionMarkingEnabled != 0 { + flags = append(flags, "congestion-marking") + } + return strings.Join(flags, ", ") +} diff --git a/tools/nfdc/nfdc_status_report_test.go b/tools/nfdc/nfdc_status_report_test.go new file mode 100644 index 00000000..3e8c90f9 --- /dev/null +++ b/tools/nfdc/nfdc_status_report_test.go @@ -0,0 +1,325 @@ +package nfdc + +import ( + "encoding/json" + "testing" + + dvconfig "github.com/named-data/ndnd/dv/config" + dvtlv "github.com/named-data/ndnd/dv/tlv" + enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/engine" + "github.com/named-data/ndnd/std/engine/face" + "github.com/named-data/ndnd/std/ndn" + mgmt "github.com/named-data/ndnd/std/ndn/mgmt_2022" + spec "github.com/named-data/ndnd/std/ndn/spec_2022" + "github.com/stretchr/testify/require" +) + +func TestNDNdStatusJSONFields(t *testing.T) { + zero := uint64(0) + report := &ndndStatusReport{ + GeneratedAt: "2026-10-26T00:00:00Z", + General: ndndGeneralStatus{NInData: ^uint64(0), NFIBEntries: 3}, + Faces: ndndFaces{Entries: []ndndFace{{ID: 7, MTU: &zero, NInBytes: 1<<53 + 1}}}, + FIB: ndndFIB{Entries: []ndndFIBEntry{{Prefix: "/app/data", + NextHops: []ndndNextHop{{FaceID: 7, Cost: ^uint64(0)}}, + }}}, + ContentStore: ndndContentStore{Capacity: 1000, Entries: 2, Hits: ^uint64(0)}, + RIB: ndndRIB{Entries: []ndndRIBEntry{{Prefix: "/app/data", + Routes: []ndndRoute{{FaceID: 7, Cost: ^uint64(0), ExpirationMillis: &zero}}, + }}}, + DV: ndndDVStatus{Available: true, DatasetAvailable: true, NNeighbors: 1, NPsdEntries: 3, + Routers: []ndndDVRouter{{Name: "/example/a", Cost: 2, Prefixes: []ndndDVPrefix{{Name: "/app/data", Multicast: true, NotBefore: "20260101T000000", NotAfter: "20990101T000000"}}}}, + }, + } + wire, err := encodeNdndStatusJSON(report) + require.NoError(t, err) + var parsed ndndStatusReport + require.NoError(t, json.Unmarshal(wire, &parsed)) + require.Equal(t, report, &parsed) + var document map[string]json.RawMessage + require.NoError(t, json.Unmarshal(wire, &document)) + require.Contains(t, document, "generatedAt") + require.NotContains(t, document, "XMLName") + var general map[string]json.RawMessage + require.NoError(t, json.Unmarshal(document["general"], &general)) + require.Equal(t, `"18446744073709551615"`, string(general["nInData"])) + require.Equal(t, `"0"`, string(general["nInInterests"])) + require.Equal(t, "3", string(general["nFibEntries"])) + var faces struct { + Entries []map[string]json.RawMessage `json:"entries"` + } + require.NoError(t, json.Unmarshal(document["faces"], &faces)) + require.Equal(t, `"7"`, string(faces.Entries[0]["faceId"])) + require.Equal(t, `"9007199254740993"`, string(faces.Entries[0]["nInBytes"])) + require.Equal(t, "0", string(faces.Entries[0]["mtu"])) + require.NotContains(t, faces.Entries[0], "expirationMilliseconds") + var dv struct { + Neighbors json.RawMessage `json:"nNeighbors"` + Routers []struct { + Cost json.RawMessage `json:"cost"` + Prefixes []struct { + Multicast bool `json:"multicast"` + } `json:"prefixes"` + } `json:"routers"` + } + require.NoError(t, json.Unmarshal(document["dv"], &dv)) + require.Equal(t, "2", string(dv.Routers[0].Cost)) + require.True(t, dv.Routers[0].Prefixes[0].Multicast) + require.Equal(t, "1", string(dv.Neighbors)) + var cs map[string]json.RawMessage + require.NoError(t, json.Unmarshal(document["contentStore"], &cs)) + require.Equal(t, "1000", string(cs["capacity"])) + require.Equal(t, "2", string(cs["entries"])) + require.Equal(t, `"18446744073709551615"`, string(cs["hits"])) + for _, table := range []struct{ section, routes string }{{"fib", "nextHop"}, {"rib", "route"}} { + var entries struct { + Entries []map[string]json.RawMessage `json:"entries"` + } + require.NoError(t, json.Unmarshal(document[table.section], &entries)) + var routes []map[string]json.RawMessage + require.NoError(t, json.Unmarshal(entries.Entries[0][table.routes], &routes)) + require.Equal(t, `"18446744073709551615"`, string(routes[0]["cost"])) + require.Equal(t, `"7"`, string(routes[0]["faceId"])) + if table.section == "rib" { + require.Equal(t, "0", string(routes[0]["expirationMilliseconds"])) + } + } +} + +func TestStatusReportRejectsInvalidFormatBeforeConnecting(t *testing.T) { + tool := Tool{statusFormat: "yaml"} + require.ErrorContains(t, tool.RunStatusReport(nil, nil), "unsupported status format") + require.Nil(t, tool.engine) +} + +func TestDVRoutingDatasetCostBounds(t *testing.T) { + router := statusTrustName(t, "/example/a") + prefix := statusTrustName(t, "/app/data") + for _, tc := range []struct { + name string + routerCost uint64 + err string + }{ + {"zero", 0, ""}, + {"maximum-reachable", dvconfig.CostInfinity - 1, ""}, + {"router-infinity", dvconfig.CostInfinity, "router cost"}, + {"router-max-uint64", ^uint64(0), "router cost"}, + } { + t.Run(tc.name, func(t *testing.T) { + dataset := &dvtlv.RoutingStatus{ + Status: &dvtlv.Status{RouterName: &dvtlv.Destination{Name: router}}, + Routers: []*dvtlv.RouterStatus{{Name: router, Cost: tc.routerCost, + Prefixes: []*dvtlv.PrefixOpAdd{{Name: prefix, Multicast: true}}, + }}, + } + status, err := ndndDVStatusFromDataset(dataset.Encode(), router) + if tc.err != "" { + require.ErrorContains(t, err, tc.err) + require.False(t, status.DatasetAvailable) + require.Empty(t, status.Routers) + return + } + require.NoError(t, err) + require.True(t, status.DatasetAvailable) + require.Equal(t, tc.routerCost, status.Routers[0].Cost) + }) + } +} + +type statusPacketTestEngine struct { + ndn.Engine + express func(*ndn.EncodedInterest, ndn.ExpressCallbackFunc) error +} + +func (e statusPacketTestEngine) Express(packet *ndn.EncodedInterest, callback ndn.ExpressCallbackFunc) error { + return e.express(packet, callback) +} + +func TestFetchDVStatusInterestIncludesNonce(t *testing.T) { + base := engine.NewBasicEngine(face.NewDummyFace()) + tool := Tool{engine: statusPacketTestEngine{Engine: base, express: func(packet *ndn.EncodedInterest, callback ndn.ExpressCallbackFunc) error { + interest, _, err := base.Spec().ReadInterest(enc.NewWireView(packet.Wire)) + require.NoError(t, err) + require.True(t, interest.Name().Equal(dvStatusName())) + require.True(t, interest.Nonce().IsSet(), "NDNd drops Interests without a nonce") + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultData}) + return nil + }}} + _, err := tool.fetchDataPacket(dvStatusName()) + require.NoError(t, err) +} + +func TestNDNdStatusXMLOmitsUnavailableCounters(t *testing.T) { + report := &ndndStatusReport{ + GeneratedAt: "2026-10-17T00:00:00Z", + General: ndndGeneralStatus{ + Version: "test-version", + NFIBEntries: 2, + NPITEntries: 3, + NInInterests: 5, + }, + } + + encoded, err := encodeNdndStatusXML(report) + require.NoError(t, err) + text := string(encoded) + require.Contains(t, text, "2") + require.NotContains(t, text, "nNameTreeEntries") + require.NotContains(t, text, "nMeasurementsEntries") + require.NotContains(t, text, "nInNacks") + require.NotContains(t, text, "nOutNacks") +} + +func TestNDNdStatusXMLFields(t *testing.T) { + expiration := uint64(900) + markInterval := uint64(1000) + congestionThreshold := uint64(2000) + report := &ndndStatusReport{ + Faces: ndndFaces{Entries: []ndndFace{{ + ID: 7, + ExpirationMilliseconds: &expiration, + BaseCongestionMarkInterval: &markInterval, + DefaultCongestionThreshold: &congestionThreshold, + }}}, + Strategies: ndndStrategies{Entries: []ndndStrategy{{ + Prefix: "/ndn", + Strategy: "/strategy/example", + }}}, + } + encoded, err := encodeNdndStatusXML(report) + require.NoError(t, err) + xmlText := string(encoded) + + for _, field := range []string{ + ``, + `900`, + `1000`, + `2000`, + } { + require.Contains(t, xmlText, field) + } + require.NotContains(t, xmlText, "nInNacks") + require.NotContains(t, xmlText, "nOutNacks") +} + +func TestNDNdDVStatusFromWire(t *testing.T) { + status := &dvtlv.Status{ + Version: "test-version", + NetworkName: &dvtlv.Destination{Name: enc.Name{enc.NewGenericComponent("ndn")}}, + RouterName: &dvtlv.Destination{Name: enc.Name{enc.NewGenericComponent("router")}}, + NRibEntries: 3, + NNeighbors: 2, + NFibEntries: 4, + } + + parsed, err := ndndDVStatusFromWire(status.Encode()) + require.NoError(t, err) + require.Equal(t, ndndDVStatus{ + Available: true, + Version: "test-version", + NetworkName: "/ndn", + RouterName: "/router", + NRibEntries: 3, + NNeighbors: 2, + NFibEntries: 4, + }, parsed) +} + +func TestNDNdDVRoutingDataset(t *testing.T) { + router, err := enc.NameFromStr("/example/a") + require.NoError(t, err) + nextHop, err := enc.NameFromStr("/example/b") + require.NoError(t, err) + prefix, err := enc.NameFromStr("/app/data") + require.NoError(t, err) + dataset := &dvtlv.RoutingStatus{ + Status: &dvtlv.Status{RouterName: &dvtlv.Destination{Name: router}}, + Routers: []*dvtlv.RouterStatus{{ + Name: nextHop, NextHop: &dvtlv.Destination{Name: nextHop}, Cost: 2, + Prefixes: []*dvtlv.PrefixOpAdd{{Name: prefix, Multicast: true, ValidityPeriod: &spec.ValidityPeriod{NotBefore: "20260101T000000", NotAfter: "20990101T000000"}}}, + }}, + } + status, err := ndndDVStatusFromDataset(dataset.Encode(), router) + require.NoError(t, err) + require.True(t, status.DatasetAvailable) + require.Equal(t, []ndndDVRouter{{Name: "/example/b", NextHop: "/example/b", Cost: 2, + Prefixes: []ndndDVPrefix{{Name: "/app/data", Multicast: true, NotBefore: "20260101T000000", NotAfter: "20990101T000000"}}, + }}, status.Routers) + report := &ndndStatusReport{DV: status} + html, err := renderNDNdStatusHTML(report) + require.NoError(t, err) + require.Contains(t, string(html), "Reachable routers and prefix announcements") + require.Contains(t, string(html), "/app/data (multicast)") + xml, err := encodeNdndStatusXML(report) + require.NoError(t, err) + require.Contains(t, string(xml), `datasetAvailable="true"`) + require.Contains(t, string(xml), ``) + require.Contains(t, string(xml), ``) + _, err = ndndDVStatusFromDataset(dataset.Encode(), nextHop) + require.ErrorContains(t, err, "requested router") + _, err = ndndDVStatusFromDataset((&dvtlv.RoutingStatus{}).Encode(), router) + require.Error(t, err) +} + +func TestNDNdDVRoutingDatasetUnavailable(t *testing.T) { + html, err := renderNDNdStatusHTML(&ndndStatusReport{DV: ndndDVStatus{Available: true}}) + require.NoError(t, err) + require.Contains(t, string(html), "DV routing dataset unavailable") + require.NotContains(t, string(html), "No reachable routers") +} + +func TestNDNdDVStatusName(t *testing.T) { + require.Equal(t, "/localhost/dv/status", dvStatusName().String()) +} + +func TestNDNdStatusFaceFlags(t *testing.T) { + flags := mgmt.FaceFlagLocalFieldsEnabled | mgmt.FaceFlagLpReliabilityEnabled | mgmt.FaceFlagCongestionMarkingEnabled + require.Equal(t, "local-fields, lp-reliability, congestion-marking", faceFlagsName(flags)) +} + +func TestNDNdStatusHTMLRenderingEscapesValues(t *testing.T) { + mtu := uint64(1450) + expiration := uint64(900) + report := &ndndStatusReport{ + General: ndndGeneralStatus{Version: ``}, + Faces: ndndFaces{Entries: []ndndFace{{MTU: &mtu, ExpirationMilliseconds: &expiration}}}, + DV: ndndDVStatus{Available: false}, + } + encoded, err := renderNDNdStatusHTML(report) + require.NoError(t, err) + html := string(encoded) + require.Contains(t, html, "") + require.Contains(t, html, "NDNd Status") + require.Contains(t, html, `<script>alert("x")</script>`) + require.NotContains(t, html, ``) + require.Contains(t, html, ">1450") + require.Contains(t, html, ">900") + require.NotContains(t, html, "0x") + require.Contains(t, html, "DV status unavailable") +} + +func TestNDNdContentStoreMissingPayload(t *testing.T) { + report, err := ndndContentStoreFromStatus(&mgmt.CsInfoMsg{}) + require.ErrorContains(t, err, "no CsInfo payload") + require.Equal(t, ndndContentStore{}, report) +} + +func TestNDNdContentStoreStatusMapping(t *testing.T) { + report, err := ndndContentStoreFromStatus(&mgmt.CsInfoMsg{CsInfo: &mgmt.CsInfo{ + Capacity: 1024, + Flags: mgmt.CsEnableAdmit, + NCsEntries: 12, + NHits: 34, + NMisses: 56, + }}) + require.NoError(t, err) + require.Equal(t, ndndContentStore{ + Capacity: 1024, + Admit: true, + Serve: false, + Entries: 12, + Hits: 34, + Misses: 56, + }, report) +} diff --git a/tools/nfdc/nfdc_status_server.go b/tools/nfdc/nfdc_status_server.go new file mode 100644 index 00000000..a04e5983 --- /dev/null +++ b/tools/nfdc/nfdc_status_server.go @@ -0,0 +1,102 @@ +package nfdc + +import ( + "bytes" + _ "embed" + "errors" + "fmt" + "html/template" + "net/http" + "sync" + "time" + + "github.com/named-data/ndnd/std/log" + "github.com/spf13/cobra" +) + +//go:embed ndnd-status.html +var ndndStatusHTMLSource string + +var ndndStatusHTML = template.Must(template.New("ndnd-status").Funcs(template.FuncMap{ + "bitList": formatBitList, + "optionalUint": func(value *uint64) string { + if value == nil { + return "" + } + return fmt.Sprint(*value) + }, +}).Parse(ndndStatusHTMLSource)) + +func (t *Tool) RunStatusServer(_ *cobra.Command, _ []string) error { + if err := t.configureStatusTrust(); err != nil { + return err + } + t.Start() + defer t.Stop() + + mux := statusServerHandler(t.buildNdndStatusReport, log.Default()) + + server := &http.Server{Addr: ":6380", Handler: mux, ReadHeaderTimeout: 5 * time.Second} + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("NDNd status server failed: %w", err) + } + return nil +} + +func statusServerHandler(buildReport func() (*ndndStatusReport, error), logger *log.Logger) http.Handler { + var reportMu sync.Mutex + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.NotFound(w, r) + return + } + encode := renderNDNdStatusHTML + contentType := "text/html; charset=utf-8" + switch r.URL.Path { + case "/": + case "/status.xml": + contentType = "application/xml; charset=utf-8" + encode = encodeNdndStatusXML + case "/status.json": + contentType = "application/json; charset=utf-8" + encode = encodeNdndStatusJSON + default: + http.NotFound(w, r) + return + } + + if !reportMu.TryLock() { + http.Error(w, "NDNd status is temporarily unavailable", http.StatusServiceUnavailable) + return + } + report, err := func() (*ndndStatusReport, error) { + defer reportMu.Unlock() + return buildReport() + }() + if err != nil { + logger.Error(nil, "Failed to collect NDNd status report", "path", r.URL.Path, "err", err) + http.Error(w, "NDNd status is temporarily unavailable", http.StatusServiceUnavailable) + return + } + + encoded, err := encode(report) + if err != nil { + logger.Error(nil, "Failed to encode NDNd status report", "path", r.URL.Path, "err", err) + http.Error(w, "NDNd status is temporarily unavailable", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Length", fmt.Sprint(len(encoded))) + _, _ = w.Write(encoded) + }) + return mux +} + +func renderNDNdStatusHTML(report *ndndStatusReport) ([]byte, error) { + var output bytes.Buffer + if err := ndndStatusHTML.Execute(&output, report); err != nil { + return nil, err + } + return output.Bytes(), nil +} diff --git a/tools/nfdc/nfdc_status_server_test.go b/tools/nfdc/nfdc_status_server_test.go new file mode 100644 index 00000000..44ac6e92 --- /dev/null +++ b/tools/nfdc/nfdc_status_server_test.go @@ -0,0 +1,165 @@ +package nfdc + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "errors" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/named-data/ndnd/std/log" + "github.com/stretchr/testify/require" +) + +func TestStatusServerServesPage(t *testing.T) { + handler := statusServerHandler(func() (*ndndStatusReport, error) { + return &ndndStatusReport{DV: ndndDVStatus{ + Available: true, DatasetAvailable: true, + Routers: []ndndDVRouter{{Name: "/example/a", Prefixes: []ndndDVPrefix{{Name: "/app/data", Multicast: true}}}}, + }}, nil + }, log.Default()) + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/", nil)) + require.Equal(t, http.StatusOK, response.Code) + require.Contains(t, response.Header().Get("Content-Type"), "text/html") + require.Contains(t, response.Body.String(), "/app/data (multicast)") +} + +func TestStatusServerServesMachineReadableReports(t *testing.T) { + report := &ndndStatusReport{ + General: ndndGeneralStatus{Version: "test<&>version", NFIBEntries: 3}, + DV: ndndDVStatus{Available: true, DatasetAvailable: true, + Routers: []ndndDVRouter{{Name: "/example/a", Prefixes: []ndndDVPrefix{{Name: "/app/data", Multicast: true}}}}, + }, + } + for _, format := range []struct { + name string + decode func([]byte, any) error + encode func(*ndndStatusReport) ([]byte, error) + }{ + {"xml", xml.Unmarshal, encodeNdndStatusXML}, + {"json", json.Unmarshal, encodeNdndStatusJSON}, + } { + t.Run(format.name, func(t *testing.T) { + calls := 0 + handler := statusServerHandler(func() (*ndndStatusReport, error) { calls++; return report, nil }, log.Default()) + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status."+format.name, nil)) + require.Equal(t, http.StatusOK, response.Code) + require.Equal(t, "application/"+format.name+"; charset=utf-8", response.Header().Get("Content-Type")) + require.Equal(t, strconv.Itoa(response.Body.Len()), response.Header().Get("Content-Length")) + require.Equal(t, 1, calls) + expected, err := format.encode(report) + require.NoError(t, err) + require.Equal(t, expected, response.Body.Bytes()) + var parsed ndndStatusReport + require.NoError(t, format.decode(response.Body.Bytes(), &parsed)) + require.Equal(t, report.General, parsed.General) + require.Equal(t, report.DV, parsed.DV) + }) + } +} + +func TestStatusServerRejectsInvalidReportRequests(t *testing.T) { + handler := statusServerHandler(nil, log.Default()) + for _, request := range []*http.Request{ + httptest.NewRequest(http.MethodPost, "/status.xml", nil), + httptest.NewRequest(http.MethodGet, "/status.xml/extra", nil), + httptest.NewRequest(http.MethodPost, "/status.json", nil), + httptest.NewRequest(http.MethodGet, "/status.json/extra", nil), + } { + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + require.Equal(t, http.StatusNotFound, response.Code) + } +} + +func TestStatusServerCollectorPanicReleasesLock(t *testing.T) { + calls := 0 + handler := statusServerHandler(func() (*ndndStatusReport, error) { + calls++ + if calls == 1 { + panic("collector failure") + } + return &ndndStatusReport{}, nil + }, log.Default()) + // net/http recovers handler panics and continues serving requests. + require.Panics(t, func() { + handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) + }) + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/", nil)) + require.Equal(t, http.StatusOK, response.Code) + require.Equal(t, 2, calls) +} + +func TestStatusServerRejectsConcurrentReports(t *testing.T) { + started, release, done := make(chan struct{}, 2), make(chan struct{}), make(chan struct{}) + handler := statusServerHandler(func() (*ndndStatusReport, error) { + started <- struct{}{} + <-release + return &ndndStatusReport{}, nil + }, log.Default()) + defer close(release) + go func() { + handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) + close(done) + }() + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("first status request did not start") + } + for _, path := range []string{"/status.xml", "/status.json"} { + response := httptest.NewRecorder() + secondDone := make(chan struct{}) + go func() { + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil)) + close(secondDone) + }() + select { + case <-secondDone: + case <-time.After(2 * time.Second): + t.Fatal("concurrent status request was queued") + } + require.Equal(t, http.StatusServiceUnavailable, response.Code) + } + // Releasing the first request is deferred even if an assertion fails. + t.Cleanup(func() { + select { + case <-done: + case <-time.After(2 * time.Second): + t.Error("status request did not finish") + } + }) +} + +func TestStatusServerLogsFailures(t *testing.T) { + for _, tc := range []struct { + name, path, message, detail string + collectError error + status int + }{ + {"collect-html", "/", "Failed to collect NDNd status report", "private collection detail", errors.New("private collection detail"), http.StatusServiceUnavailable}, + {"collect-xml", "/status.xml", "Failed to collect NDNd status report", "private collection detail", errors.New("private collection detail"), http.StatusServiceUnavailable}, + {"collect-json", "/status.json", "Failed to collect NDNd status report", "private collection detail", errors.New("private collection detail"), http.StatusServiceUnavailable}, + {"encode-html", "/", "Failed to encode NDNd status report", "nil pointer evaluating", nil, http.StatusInternalServerError}, + } { + t.Run(tc.name, func(t *testing.T) { + var output bytes.Buffer + handler := statusServerHandler(func() (*ndndStatusReport, error) { return nil, tc.collectError }, log.NewText(&output)) + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, tc.path, nil)) + require.Equal(t, tc.status, response.Code) + require.Contains(t, response.Body.String(), "NDNd status is temporarily unavailable") + require.NotContains(t, response.Body.String(), tc.detail) + require.Contains(t, output.String(), tc.message) + require.Contains(t, output.String(), tc.detail) + require.Contains(t, output.String(), "path="+tc.path) + }) + } +} diff --git a/tools/nfdc/nfdc_status_trust.go b/tools/nfdc/nfdc_status_trust.go new file mode 100644 index 00000000..424f8d9d --- /dev/null +++ b/tools/nfdc/nfdc_status_trust.go @@ -0,0 +1,68 @@ +package nfdc + +import ( + "fmt" + "os" + + dvconfig "github.com/named-data/ndnd/dv/config" + enc "github.com/named-data/ndnd/std/encoding" + spec "github.com/named-data/ndnd/std/ndn/spec_2022" + "github.com/named-data/ndnd/std/object/storage" + sec "github.com/named-data/ndnd/std/security" + "github.com/named-data/ndnd/std/security/keychain" + "github.com/named-data/ndnd/std/security/trust_schema" +) + +func (t *Tool) configureStatusTrust() error { + t.statusTrust = nil + if len(t.statusTrustAnchors) == 0 { + if t.statusTrustSchemaPath != "" { + return fmt.Errorf("--trust-schema requires at least one --trust-anchor") + } + return nil + } + kc := keychain.NewKeyChainMem(storage.NewMemoryStore()) + var anchors []enc.Name + for _, path := range t.statusTrustAnchors { + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read DV trust anchor %s: %w", path, err) + } + _, certs, err := sec.DecodeFile(content) + if err != nil { + return fmt.Errorf("decode DV trust anchor %s: %w", path, err) + } + if len(certs) == 0 { + return fmt.Errorf("DV trust anchor %s must contain an NDN certificate", path) + } + for _, wire := range certs { + if err := kc.InsertCert(wire); err != nil { + return fmt.Errorf("load DV trust anchor %s: %w", path, err) + } + cert, _, err := (spec.Spec{}).ReadData(enc.NewBufferView(wire)) + if err != nil { + return fmt.Errorf("parse DV trust anchor %s: %w", path, err) + } + anchors = append(anchors, cert.Name()) + } + } + schemaBytes := dvconfig.SchemaBytes + if t.statusTrustSchemaPath != "" { + var err error + schemaBytes, err = os.ReadFile(t.statusTrustSchemaPath) + if err != nil { + return fmt.Errorf("read DV status trust schema %s: %w", t.statusTrustSchemaPath, err) + } + } + schema, err := trust_schema.NewLvsSchema(schemaBytes) + if err != nil { + return fmt.Errorf("load DV status trust schema: %w", err) + } + trust, err := sec.NewTrustConfig(kc, schema, anchors) + if err != nil { + return fmt.Errorf("configure DV status validation: %w", err) + } + trust.UseDataNameFwHint = true + t.statusTrust = trust + return nil +} diff --git a/tools/nfdc/nfdc_status_trust_test.go b/tools/nfdc/nfdc_status_trust_test.go new file mode 100644 index 00000000..e0ba3417 --- /dev/null +++ b/tools/nfdc/nfdc_status_trust_test.go @@ -0,0 +1,248 @@ +package nfdc + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + dvconfig "github.com/named-data/ndnd/dv/config" + dvtlv "github.com/named-data/ndnd/dv/tlv" + enc "github.com/named-data/ndnd/std/encoding" + "github.com/named-data/ndnd/std/engine" + "github.com/named-data/ndnd/std/engine/face" + "github.com/named-data/ndnd/std/ndn" + spec "github.com/named-data/ndnd/std/ndn/spec_2022" + "github.com/named-data/ndnd/std/object" + "github.com/named-data/ndnd/std/object/storage" + sec "github.com/named-data/ndnd/std/security" + "github.com/named-data/ndnd/std/security/signer" + "github.com/stretchr/testify/require" +) + +func statusTrustName(t *testing.T, value string) enc.Name { + t.Helper() + name, err := enc.NameFromStr(value) + require.NoError(t, err) + return name +} + +func statusTrustProducer(t *testing.T, router string, routerCost uint64, keyComponent string) (ndn.Store, []byte) { + t.Helper() + store := storage.NewMemoryStore() + rootKey, err := signer.KeygenEd25519(sec.MakeKeyName(statusTrustName(t, "/example"))) + require.NoError(t, err) + before, after := time.Now().Add(-time.Hour), time.Now().Add(time.Hour) + root, err := sec.SelfSign(sec.SignCertArgs{Signer: rootKey, NotBefore: before, NotAfter: after}) + require.NoError(t, err) + key, err := signer.KeygenEd25519(sec.MakeKeyName(statusTrustName(t, router+"/"+keyComponent))) + require.NoError(t, err) + secret, err := signer.MarshalSecretToData(key) + require.NoError(t, err) + cert, err := sec.SignCert(sec.SignCertArgs{Signer: rootKey, Data: secret, + IssuerId: enc.NewGenericComponent("network"), NotBefore: before, NotAfter: after}) + require.NoError(t, err) + for _, wire := range []enc.Wire{root, cert} { + data, _, err := (spec.Spec{}).ReadData(enc.NewWireView(wire)) + require.NoError(t, err) + require.NoError(t, store.Put(data.Name(), wire.Join())) + } + requested := statusTrustName(t, "/example/a") + status := &dvtlv.Status{Version: "test", NetworkName: &dvtlv.Destination{Name: statusTrustName(t, "/example")}, + RouterName: &dvtlv.Destination{Name: requested}} + dataset := &dvtlv.RoutingStatus{Status: status, Routers: []*dvtlv.RouterStatus{{Name: requested, Cost: routerCost}}} + for i := range 1000 { + dataset.Routers[0].Prefixes = append(dataset.Routers[0].Prefixes, + &dvtlv.PrefixOpAdd{Name: statusTrustName(t, fmt.Sprintf("/application/prefix-%04d", i)), Multicast: i%2 == 0}) + } + _, err = object.Produce(ndn.ProduceArgs{ + Name: dvconfig.StatusDatasetPrefix(requested).WithVersion(1), Content: dataset.Encode(), + }, store, key) + require.NoError(t, err) + summary, err := (spec.Spec{}).MakeData(dvStatusName(), &ndn.DataConfig{}, status.Encode(), nil) + require.NoError(t, err) + require.NoError(t, store.Put(dvStatusName(), summary.Wire.Join())) + return store, root.Join() +} + +// Deliver encoded packets on an engine queue. Only management and dataset +// names are routable; certificate Interests must carry a dataset forwarding hint. +type statusTrustEngine struct { + ndn.Engine + store ndn.Store + queue chan func() + tamper string + metadataRequests int + tamperedSegments []uint64 +} + +func (e *statusTrustEngine) Post(fn func()) { e.queue <- fn } + +func (e *statusTrustEngine) Express(packet *ndn.EncodedInterest, callback ndn.ExpressCallbackFunc) error { + interest, _, err := e.Spec().ReadInterest(enc.NewWireView(packet.Wire)) + if err != nil { + return err + } + e.Post(func() { + prefix, _ := enc.NameFromStr("/example/a/32=DV/32=STATUS") + name := interest.Name() + routed := name.Equal(dvStatusName()) || prefix.IsPrefix(name) + for _, hint := range interest.ForwardingHint() { + routed = routed || prefix.IsPrefix(hint) + } + if !routed { + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultError, Error: fmt.Errorf("no certificate route")}) + return + } + if name.At(-1).IsKeyword("metadata") { + e.metadataRequests++ + } + wire, err := e.store.Get(name, interest.CanBePrefix()) + if err != nil || wire == nil { + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultError, Error: fmt.Errorf("packet unavailable")}) + return + } + tamperSegment := name.At(-1).IsSegment() && + ((e.tamper == "segment-0" && name.At(-1).NumberVal() == 0) || + (e.tamper == "segment-1" && name.At(-1).NumberVal() == 1)) + if prefix.IsPrefix(name) && ((e.tamper == "metadata" && name.At(-1).IsKeyword("metadata")) || tamperSegment) { + wire = append([]byte(nil), wire...) + wire[len(wire)-1] ^= 1 // Change the signature while keeping the TLV well formed. + if tamperSegment { + e.tamperedSegments = append(e.tamperedSegments, name.At(-1).NumberVal()) + } + } + data, covered, err := e.Spec().ReadData(enc.NewBufferView(wire)) + if err != nil { + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultError, Error: err}) + return + } + callback(ndn.ExpressCallbackArgs{Result: ndn.InterestResultData, Data: data, SigCovered: covered, RawData: enc.Wire{wire}}) + }) + return nil +} + +func TestStatusReportValidatesDVDataset(t *testing.T) { + for _, scenario := range []string{"valid", "metadata", "segment-0", "segment-1", "wrong-router", "untrusted-root", "no-anchors", "invalid-cost", "custom-schema", "custom-schema-default", "custom-schema-wrong-key"} { + t.Run(scenario, func(t *testing.T) { + router := "/example/a" + if scenario == "wrong-router" { + router = "/example/b" + } + cost := uint64(0) + if scenario == "invalid-cost" { + cost = 1<<53 + 1 + } + keyComponent := "32=DV" + if scenario == "custom-schema" || scenario == "custom-schema-default" { + keyComponent = "router-key" + } + store, root := statusTrustProducer(t, router, cost, keyComponent) + if scenario == "untrusted-root" { + _, root = statusTrustProducer(t, "/example/a", 0, "32=DV") + } + transport := &statusTrustEngine{Engine: engine.NewBasicEngine(face.NewDummyFace()), + store: store, queue: make(chan func(), 512), tamper: scenario} + stop, stopped := make(chan struct{}), make(chan struct{}) + go func() { + defer close(stopped) + for { + select { + case fn := <-transport.queue: + fn() + case <-stop: + return + } + } + }() + defer func() { close(stop); <-stopped }() + tool := Tool{engine: transport} + if scenario == "custom-schema" || scenario == "custom-schema-wrong-key" { + tool.statusTrustSchemaPath = "testdata/status-custom-schema.tlv" + } + if scenario != "no-anchors" { + path := filepath.Join(t.TempDir(), "root.cert") + pem, err := sec.PemEncode(root) + require.NoError(t, err) + require.NoError(t, os.WriteFile(path, pem, 0600)) + tool.statusTrustAnchors = []string{path} + } + require.NoError(t, tool.configureStatusTrust()) + report := ndndStatusReport{General: ndndGeneralStatus{NFIBEntries: 7}} + err := tool.populateDVStatus(&report) + if scenario == "valid" || scenario == "custom-schema" || scenario == "no-anchors" { + require.NoError(t, err) + } else if scenario == "invalid-cost" { + require.ErrorContains(t, err, "parse DV routing dataset") + require.ErrorContains(t, err, "router cost") + } else { + require.ErrorContains(t, err, "fetch or validate DV routing dataset") + if scenario == "custom-schema-default" || scenario == "custom-schema-wrong-key" { + require.ErrorContains(t, err, "trust schema mismatch") + } + } + require.True(t, report.DV.Available) + require.Equal(t, uint64(7), report.General.NFIBEntries) + require.Equal(t, scenario == "valid" || scenario == "custom-schema", report.DV.DatasetAvailable) + if scenario == "valid" || scenario == "custom-schema" { + require.Len(t, report.DV.Routers[0].Prefixes, 1000) + } else { + require.Empty(t, report.DV.Routers) + } + if scenario == "no-anchors" { + require.Zero(t, transport.metadataRequests) + } + if scenario == "segment-0" || scenario == "segment-1" { + require.ErrorContains(t, err, "validate seg failed") + segment := uint64(0) + if scenario == "segment-1" { + segment = 1 + } + require.Equal(t, []uint64{segment}, transport.tamperedSegments) + } + }) + } +} + +func TestStatusTrustRejectsInvalidFiles(t *testing.T) { + path := filepath.Join(t.TempDir(), "root.cert") + tool := Tool{statusTrustAnchors: []string{path}} + require.Error(t, tool.configureStatusTrust()) + for _, content := range [][]byte{nil, []byte("not a certificate")} { + require.NoError(t, os.WriteFile(path, content, 0600)) + err := tool.configureStatusTrust() + require.ErrorContains(t, err, "decode DV trust anchor") + if len(content) == 0 { + require.ErrorContains(t, err, "empty keychain entry") + } else { + require.ErrorContains(t, err, "no valid keychain entry found") + } + require.Nil(t, tool.statusTrust) + } +} + +func TestStatusTrustRejectsInvalidSchema(t *testing.T) { + _, root := statusTrustProducer(t, "/example/a", 0, "32=DV") + dir := t.TempDir() + anchor := filepath.Join(dir, "root.cert") + require.NoError(t, os.WriteFile(anchor, root, 0600)) + path := filepath.Join(dir, "schema.tlv") + tool := Tool{statusTrustAnchors: []string{anchor}} + require.NoError(t, tool.configureStatusTrust()) + require.NotNil(t, tool.statusTrust) + tool.statusTrustSchemaPath = path + require.ErrorContains(t, tool.configureStatusTrust(), "read DV status trust schema") + require.Nil(t, tool.statusTrust) + for _, data := range [][]byte{nil, []byte("not a compiled schema")} { + require.NoError(t, os.WriteFile(path, data, 0600)) + require.ErrorContains(t, tool.configureStatusTrust(), "load DV status trust schema") + require.Nil(t, tool.statusTrust) + } +} + +func TestStatusSchemaRequiresTrustAnchor(t *testing.T) { + tool := Tool{statusTrustSchemaPath: "testdata/status-custom-schema.tlv"} + require.ErrorContains(t, tool.configureStatusTrust(), "--trust-schema requires at least one --trust-anchor") + require.Nil(t, tool.statusTrust) +} diff --git a/tools/nfdc/testdata/status-custom-schema.tlv b/tools/nfdc/testdata/status-custom-schema.tlv new file mode 100644 index 00000000..6a017a29 Binary files /dev/null and b/tools/nfdc/testdata/status-custom-schema.tlv differ diff --git a/tools/nfdc/testdata/status-custom-schema.trust b/tools/nfdc/testdata/status-custom-schema.trust new file mode 100644 index 00000000..e9f942c6 --- /dev/null +++ b/tools/nfdc/testdata/status-custom-schema.trust @@ -0,0 +1,24 @@ +// Network name from config +#network: net10 +#network: net20/net21 +#network: net30/net31/net32 +// Router name from config +#router: #network/router + +// Advertisement data and broadcast +#advertisement_data: /"localhop"/#router/"32=DV"/"32=ADV"/_ <= #router_cert + +// Read-only routing status dataset +#status_data: #router/"32=DV"/"32=STATUS" <= #router_cert + +// Prefix state database +#prefix_state: #network/"32=DV"/"32=PSD" +// Prefix state database data +#prefix_state_data: #prefix_state/#router/_/_ <= #router_cert + +// Certificate definitions +#network_cert: #network/#KEY +#router_cert: #router/"router-key"/#KEY <= #network_cert + +// Standard NDN conventions +#KEY: "KEY"/_/_/_