From f87328af7435d5c8f836ce7b4ed28334a2702dee Mon Sep 17 00:00:00 2001 From: Harsh23Kashyap <55448981+Harsh23Kashyap@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:37:31 +0530 Subject: [PATCH] fw: implement rib/announce management command Implements the rib/announce management command (closes #195), which previously always returned 501. The PrefixAnnouncement Data packet carried in the command Interest's ApplicationParameters is parsed for its announced prefix (the name before the PA keyword component, optionally followed by version and segment components), its required ExpirationPeriod, and its optional route Cost. A route with origin prefixann (129) toward the face the command arrived on is inserted into the RIB and expires after the announced ExpirationPeriod, mirroring NFD's rib-manager. The control response carries the created route's parameters. PrefixAnnouncement signatures are not validated, as YaNFD has no trust schema configured for management. --- fw/mgmt/rib.go | 105 ++++++++++++++++++++++++++++++++++++++-- fw/mgmt/rib_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 fw/mgmt/rib_test.go diff --git a/fw/mgmt/rib.go b/fw/mgmt/rib.go index f97c7796..04d353de 100644 --- a/fw/mgmt/rib.go +++ b/fw/mgmt/rib.go @@ -8,6 +8,7 @@ package mgmt import ( + "errors" "strconv" "time" @@ -15,6 +16,7 @@ import ( "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" @@ -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) @@ -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”. diff --git a/fw/mgmt/rib_test.go b/fw/mgmt/rib_test.go new file mode 100644 index 00000000..b8599301 --- /dev/null +++ b/fw/mgmt/rib_test.go @@ -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) +}