Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
105 changes: 102 additions & 3 deletions fw/mgmt/rib.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
package mgmt

import (
"errors"
"strconv"
"time"

"github.com/named-data/ndnd/fw/core"
"github.com/named-data/ndnd/fw/face"
"github.com/named-data/ndnd/fw/table"
enc "github.com/named-data/ndnd/std/encoding"
"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/named-data/ndnd/std/types/optional"
Expand Down Expand Up @@ -158,7 +160,7 @@ func (r *RIBModule) unregister(interest *Interest) {
core.Log.Info(r, "Removed route", "name", params.Name, "faceid", faceID, "origin", origin)
}

// (AI GENERATED DESCRIPTION): Handles a PrefixAnnouncement Interest by validating its name and application parameters and replying with a 501 Not Implemented response, since the announcement logic is not yet implemented.
// Handles a rib/announce Interest by validating the embedded PrefixAnnouncement Data packet, inserting the announced prefix into the RIB with origin prefixann toward the face the command arrived on, and replying with the created route parameters.
func (r *RIBModule) announce(interest *Interest) {
if len(interest.Name()) != len(LOCAL_PREFIX)+3 || interest.Name()[len(LOCAL_PREFIX)+2].Typ != enc.TypeParametersSha256DigestComponent {
r.manager.sendCtrlResp(interest, 400, "Name is incorrect", nil)
Expand All @@ -177,10 +179,107 @@ func (r *RIBModule) announce(interest *Interest) {
r.manager.sendCtrlResp(interest, 400, "PrefixAnnouncement is invalid", nil)
return
}
if data != nil {

prefix, expiration, cost, err := parsePrefixAnnouncement(data)
if err != nil {
core.Log.Warn(r, "Invalid PrefixAnnouncement", "err", err)
r.manager.sendCtrlResp(interest, 400, "PrefixAnnouncement is invalid", nil)
return
}

// The announced route always points back toward the face the command
// arrived on, and expires when the PrefixAnnouncement says it should,
// mirroring NFD's rib-manager.
faceID := interest.inFace.Unwrap()
flags := uint64(mgmt.RouteFlagChildInherit)
table.Rib.AddEncRoute(prefix, &table.Route{
FaceID: faceID,
Origin: uint64(mgmt.RouteOriginPrefixAnn),
Cost: cost,
Flags: flags,
ExpirationPeriod: &expiration,
})

core.Log.Info(r, "Created announced route", "name", prefix, "faceid", faceID,
"cost", cost, "expires", expiration)

r.manager.sendCtrlResp(interest, 200, "OK", &mgmt.ControlArgs{
Name: prefix,
FaceId: optional.Some(faceID),
Origin: optional.Some(uint64(mgmt.RouteOriginPrefixAnn)),
Cost: optional.Some(cost),
Flags: optional.Some(flags),
ExpirationPeriod: optional.Some(uint64(expiration.Milliseconds())),
})
}

// parsePrefixAnnouncement extracts the announced prefix, route expiration and
// route cost from a PrefixAnnouncement Data packet. The announced prefix is
// the portion of the Data name before the PA keyword component, which may be
// followed by version and segment components. ValidityPeriod in the content
// is ignored; the route expiration comes from ExpirationPeriod alone.
func parsePrefixAnnouncement(data ndn.Data) (enc.Name, time.Duration, uint64, error) {
name := data.Name()

paIndex := -1
if len(name) >= 3 && name[len(name)-3].IsKeyword("PA") &&
name[len(name)-2].IsVersion() && name[len(name)-1].IsSegment() {
paIndex = len(name) - 3
} else if len(name) >= 1 && name[len(name)-1].IsKeyword("PA") {
paIndex = len(name) - 1
}
if paIndex < 1 {
return nil, 0, 0, errors.New("name does not contain a PA keyword component after the announced prefix")
}
prefix := name[:paIndex]

expiration := uint64(0)
hasExpiration := false
cost := uint64(0)
view := enc.NewWireView(data.Content())
for !view.IsEOF() {
typ, err := view.ReadTLNum()
if err != nil {
return nil, 0, 0, errors.New("content is not valid TLV")
}
length, err := view.ReadTLNum()
if err != nil {
return nil, 0, 0, errors.New("content is not valid TLV")
}
switch typ {
case 0x6d: // ExpirationPeriod
expiration, err = readNni(&view, int(length))
hasExpiration = true
case 0x6a: // Cost
cost, err = readNni(&view, int(length))
default: // ValidityPeriod and unknown elements
err = view.Skip(int(length))
}
if err != nil {
return nil, 0, 0, errors.New("content is not valid TLV")
}
}
if !hasExpiration {
return nil, 0, 0, errors.New("content is missing ExpirationPeriod")
}

r.manager.sendCtrlResp(interest, 501, "PrefixAnnouncement not implemented yet", nil)
return prefix, time.Duration(expiration) * time.Millisecond, cost, nil
}

// readNni reads a non-negative integer TLV value of 1, 2, 4 or 8 bytes.
func readNni(view *enc.WireView, length int) (uint64, error) {
if length != 1 && length != 2 && length != 4 && length != 8 {
return 0, errors.New("non-negative integer must be 1, 2, 4 or 8 bytes")
}
buf, err := view.ReadBuf(length)
if err != nil {
return 0, err
}
val := uint64(0)
for _, b := range buf {
val = val<<8 | uint64(b)
}
return val, nil
}

// (AI GENERATED DESCRIPTION): Responds to a “/local/rib/list” Interest by collecting all current RIB entries, encoding them into a mgmt.RibStatus dataset, and sending the dataset back as a Data packet with a name derived from the Interest’s prefix and the components “rib”/“list”.
Expand Down
115 changes: 115 additions & 0 deletions fw/mgmt/rib_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* YaNFD - Yet another NDN Forwarding Daemon
*
* This file is licensed under the terms of the MIT License, as found in LICENSE.md.
*/

package mgmt

import (
"testing"
"time"

enc "github.com/named-data/ndnd/std/encoding"
"github.com/named-data/ndnd/std/ndn"
spec "github.com/named-data/ndnd/std/ndn/spec_2022"
"github.com/named-data/ndnd/std/security/signer"
"github.com/named-data/ndnd/std/types/optional"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// nniTlv encodes a TLV element holding a non-negative integer with the given
// value width, as used inside a PrefixAnnouncement content.
func nniTlv(typ byte, val uint64, width int) []byte {
out := []byte{typ, byte(width)}
for i := width - 1; i >= 0; i-- {
out = append(out, byte(val>>(8*i)))
}
return out
}

func makePrefixAnnouncement(t *testing.T, name enc.Name, content []byte) ndn.Data {
t.Helper()
encoded, err := spec.Spec{}.MakeData(name, &ndn.DataConfig{
ContentType: optional.Some(ndn.ContentTypePrefixAnnouncement),
}, enc.Wire{content}, signer.NewSha256Signer())
require.NoError(t, err)
data, _, err := spec.Spec{}.ReadData(enc.NewWireView(encoded.Wire))
require.NoError(t, err)
return data
}

func paName(t *testing.T, prefix string, suffix ...enc.Component) enc.Name {
t.Helper()
name, err := enc.NameFromStr(prefix)
require.NoError(t, err)
return append(name, suffix...)
}

// Unit test covering parsePrefixAnnouncement: NDNts-style versioned and keyword-only PA names, cost and ExpirationPeriod extraction in several NNI widths, skipping of ValidityPeriod and unknown elements, and rejection of malformed announcements.
func TestParsePrefixAnnouncement(t *testing.T) {
expiration := nniTlv(0x6d, 600000, 4)

// NDNts-style name: announced prefix + 32=PA + version + segment 0.
// A ValidityPeriod element in the content is skipped.
validity := []byte{0xfd, 0x00, 0xfd, 0x02, 0x00, 0x00}
data := makePrefixAnnouncement(t,
paName(t, "/localhost/demo-prefixann",
enc.NewKeywordComponent("PA"), enc.NewVersionComponent(123), enc.NewSegmentComponent(0)),
append(validity, expiration...))
prefix, expiry, cost, err := parsePrefixAnnouncement(data)
require.NoError(t, err)
assert.Equal(t, "/localhost/demo-prefixann", prefix.String())
assert.Equal(t, 600*time.Second, expiry)
assert.Equal(t, uint64(0), cost)

// Keyword-only name without version and segment components.
data = makePrefixAnnouncement(t,
paName(t, "/a/b", enc.NewKeywordComponent("PA")),
expiration)
prefix, _, _, err = parsePrefixAnnouncement(data)
require.NoError(t, err)
assert.Equal(t, "/a/b", prefix.String())

// Route cost is taken from the content when present.
data = makePrefixAnnouncement(t,
paName(t, "/a/b", enc.NewKeywordComponent("PA")),
append(nniTlv(0x6a, 5, 1), expiration...))
_, _, cost, err = parsePrefixAnnouncement(data)
require.NoError(t, err)
assert.Equal(t, uint64(5), cost)

// One-byte ExpirationPeriod is accepted.
data = makePrefixAnnouncement(t,
paName(t, "/a/b", enc.NewKeywordComponent("PA")),
nniTlv(0x6d, 100, 1))
_, expiry, _, err = parsePrefixAnnouncement(data)
require.NoError(t, err)
assert.Equal(t, 100*time.Millisecond, expiry)

// ExpirationPeriod is required.
data = makePrefixAnnouncement(t,
paName(t, "/a/b", enc.NewKeywordComponent("PA")),
nniTlv(0x6a, 5, 1))
_, _, _, err = parsePrefixAnnouncement(data)
require.Error(t, err)

// An NNI with a non-standard width is rejected.
data = makePrefixAnnouncement(t,
paName(t, "/a/b", enc.NewKeywordComponent("PA")),
nniTlv(0x6d, 600000, 3))
_, _, _, err = parsePrefixAnnouncement(data)
require.Error(t, err)

// Name without a PA keyword component is rejected.
data = makePrefixAnnouncement(t, paName(t, "/a/b"), expiration)
_, _, _, err = parsePrefixAnnouncement(data)
require.Error(t, err)

// Announced prefix must not be empty.
data = makePrefixAnnouncement(t,
enc.Name{enc.NewKeywordComponent("PA"), enc.NewVersionComponent(123), enc.NewSegmentComponent(0)},
expiration)
_, _, _, err = parsePrefixAnnouncement(data)
require.Error(t, err)
}
Loading