Repository navigation
dv: add routing status datasets and an HTTP status page #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
e8de6da
a9ad1f0
343b264
d320402
315a226
e186826
c8dc788
7b331d8
f3337a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,21 @@ 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. It includes general forwarder counters, faces, FIB and RIB 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's best to provide a JSON edition in addition to or in place of the XML edition.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added json support |
||
|
|
||
| ## `ndnd fw status-server` | ||
|
|
||
| The status-server command serves a server-rendered HTML status page at `/` and a machine-readable XML report at `/status.xml` over HTTP on port 6380. The XML endpoint returns `application/xml` and uses the same encoder as `status-report`, including its XSLT stylesheet reference. Both 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-server | ||
| # Open http://localhost:6380/ | ||
| curl http://localhost:6380/status.xml | ||
| ``` | ||
|
|
||
| ## `ndnd fw face-list` | ||
|
|
||
| The face list command prints the face table, which contains information about faces. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package dv | ||
|
|
||
| import ( | ||
| "slices" | ||
| "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/log" | ||
| "github.com/named-data/ndnd/std/ndn" | ||
| 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() | ||
|
|
||
| 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()), | ||
| }} | ||
| 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()}, | ||
| } | ||
| for _, prefix := range dv.pfx.GetRouter(router.Name).Prefixes { | ||
| router.Prefixes = append(router.Prefixes, &tlv.PrefixOpAdd{Name: prefix.Name.Clone(), Cost: prefix.Cost}) | ||
| } | ||
| 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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ABNF of this structure (and any nested structures not appearing elsewhere) should appear in the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added in the spec