Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/dv-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,51 @@

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
`/<router-name>/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

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in the spec

view: its general status and all reachable routers, with each router's
selected next hop, routing cost, and learned prefix announcements and costs.
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 routes 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/nlsr/status` summary remains available.

The dataset content has these TLV fields (see `dv/tlv/definitions.go`):

| 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 and Cost |

## `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.
Expand Down
15 changes: 15 additions & 0 deletions docs/fw-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.
See NFD's feature request: https://redmine.named-data.net/issues/5342

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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.
Expand Down
5 changes: 5 additions & 0 deletions dv/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ func (c *Config) RouterName() enc.Name {
return c.routerNameN
}

// 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): Retrieves and returns the advertisement sync prefix stored in the configuration as an `enc.Name`.
func (c *Config) AdvertisementSyncPrefix() enc.Name {
return c.advSyncPfxN
Expand Down
Binary file modified dv/config/schema.tlv
Binary file not shown.
3 changes: 3 additions & 0 deletions dv/config/schema.trust
Original file line number Diff line number Diff line change
Expand Up @@ -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 table Sync group
#prefix_table: #network/"32=DV"/"32=PFS"
// Prefix table data
Expand Down
22 changes: 16 additions & 6 deletions dv/dv/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ 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

// channel to stop the DV
stop chan bool
// heartbeat for outgoing Advertisements
Expand Down Expand Up @@ -98,12 +103,13 @@ 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),
}

// Initialize advertisement module
Expand Down Expand Up @@ -238,6 +244,9 @@ func (dv *Router) register() (err error) {
if err != nil {
return err
}
if err = dv.engine.AttachHandler(config.StatusDatasetPrefix(dv.config.RouterName()), dv.onStatusDataset); err != nil {
return err
}

// Register routes to forwarder
pfxs := []enc.Name{
Expand All @@ -246,6 +255,7 @@ func (dv *Router) register() (err error) {
dv.pfxSvs.SyncPrefix(),
dv.pfxSvs.DataPrefix(),
dv.config.MgmtPrefix(),
config.StatusDatasetPrefix(dv.config.RouterName()),
}
for _, prefix := range pfxs {
dv.nfdc.Exec(nfdc.NfdMgmtCmd{
Expand Down
94 changes: 94 additions & 0 deletions dv/dv/status_dataset.go
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)
}
}
117 changes: 117 additions & 0 deletions dv/dv/status_dataset_consumer_test.go
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")
}
})
}
}
Loading
Loading