ndn_config/
nfd_command.rs

1/// NFD management command name builder and parser.
2///
3/// Management command Interests use the name structure:
4/// ```text
5/// /localhost/nfd/<module>/<verb>/<ControlParameters>
6/// ```
7///
8/// where `<ControlParameters>` is a generic name component containing the
9/// binary TLV-encoded ControlParameters block (the value bytes only, without
10/// the outer type 0x68 and length — those are implicit in the name component).
11use bytes::Bytes;
12use ndn_packet::{Name, NameComponent};
13
14use crate::control_parameters::ControlParameters;
15
16/// The standard NFD management prefix: `/localhost/nfd`.
17pub const NFD_PREFIX: &[&[u8]] = &[b"localhost", b"nfd"];
18
19// ─── Command modules and verbs ───────────────────────────────────────────────
20
21/// Management module names.
22pub mod module {
23    pub const FACES: &[u8] = b"faces";
24    pub const FIB: &[u8] = b"fib";
25    pub const RIB: &[u8] = b"rib";
26    pub const ROUTING: &[u8] = b"routing";
27    pub const DISCOVERY: &[u8] = b"discovery";
28    pub const CS: &[u8] = b"cs";
29    pub const STRATEGY: &[u8] = b"strategy-choice";
30    pub const STATUS: &[u8] = b"status";
31    pub const NEIGHBORS: &[u8] = b"neighbors";
32    pub const SERVICE: &[u8] = b"service";
33    pub const MEASUREMENTS: &[u8] = b"measurements";
34    pub const CONFIG: &[u8] = b"config";
35    pub const SECURITY: &[u8] = b"security";
36    pub const LOG: &[u8] = b"log";
37}
38
39/// Command verbs per module.
40pub mod verb {
41    // config
42    pub const GET: &[u8] = b"get";
43
44    // faces
45    pub const CREATE: &[u8] = b"create";
46    pub const UPDATE: &[u8] = b"update";
47    pub const DESTROY: &[u8] = b"destroy";
48    pub const LIST: &[u8] = b"list";
49
50    // fib
51    pub const ADD_NEXTHOP: &[u8] = b"add-nexthop";
52    pub const REMOVE_NEXTHOP: &[u8] = b"remove-nexthop";
53
54    // rib
55    pub const REGISTER: &[u8] = b"register";
56    pub const UNREGISTER: &[u8] = b"unregister";
57
58    // strategy-choice
59    pub const SET: &[u8] = b"set";
60    pub const UNSET: &[u8] = b"unset";
61
62    // cs
63    pub const CONFIG: &[u8] = b"config";
64    pub const INFO: &[u8] = b"info";
65    pub const ERASE: &[u8] = b"erase";
66
67    // service
68    pub const ANNOUNCE: &[u8] = b"announce";
69    pub const WITHDRAW: &[u8] = b"withdraw";
70    pub const BROWSE: &[u8] = b"browse";
71
72    // faces extension
73    pub const COUNTERS: &[u8] = b"counters";
74
75    // security — identity
76    pub const IDENTITY_LIST: &[u8] = b"identity-list";
77    pub const IDENTITY_GENERATE: &[u8] = b"identity-generate";
78    pub const IDENTITY_DID: &[u8] = b"identity-did";
79    /// Dataset that returns the active identity status (name, is_ephemeral, pib_path).
80    pub const IDENTITY_STATUS: &[u8] = b"identity-status";
81    pub const ANCHOR_LIST: &[u8] = b"anchor-list";
82    pub const KEY_DELETE: &[u8] = b"key-delete";
83
84    // security — NDNCERT CA
85    pub const CA_INFO: &[u8] = b"ca-info";
86    pub const CA_ENROLL: &[u8] = b"ca-enroll";
87    pub const CA_TOKEN_ADD: &[u8] = b"ca-token-add";
88    pub const CA_REQUESTS: &[u8] = b"ca-requests";
89
90    // security — YubiKey PIV
91    pub const YUBIKEY_DETECT: &[u8] = b"yubikey-detect";
92    pub const YUBIKEY_GENERATE: &[u8] = b"yubikey-generate";
93
94    // security — trust schema management
95    /// Add a rule to the active trust schema.
96    /// ControlParameters.uri = `"<data_pattern> => <key_pattern>"`.
97    pub const SCHEMA_RULE_ADD: &[u8] = b"schema-rule-add";
98    /// Remove the rule at the given index.
99    /// ControlParameters.count = `rule_index`.
100    pub const SCHEMA_RULE_REMOVE: &[u8] = b"schema-rule-remove";
101    /// List all active trust schema rules (dataset, no parameters).
102    pub const SCHEMA_LIST: &[u8] = b"schema-list";
103    /// Replace the entire schema.
104    /// ControlParameters.uri = newline-separated rule strings.
105    pub const SCHEMA_SET: &[u8] = b"schema-set";
106
107    // log
108    pub const GET_FILTER: &[u8] = b"get-filter";
109    pub const SET_FILTER: &[u8] = b"set-filter";
110    pub const GET_RECENT: &[u8] = b"get-recent";
111
112    // discovery
113    pub const DVR_STATUS: &[u8] = b"dvr-status";
114    pub const DVR_CONFIG: &[u8] = b"dvr-config";
115}
116
117// ─── Name builder ────────────────────────────────────────────────────────────
118
119/// Build a management command name with embedded ControlParameters.
120///
121/// Result: `/localhost/nfd/<module>/<verb>/<params-component>`
122///
123/// The ControlParameters name component contains the **full** TLV block
124/// (type 0x68 + length + fields), matching the NFD management protocol spec
125/// and what NFD/ndnd expect.
126pub fn command_name(module: &[u8], verb: &[u8], params: &ControlParameters) -> Name {
127    let params_tlv = params.encode();
128    Name::from_components([
129        NameComponent::generic(Bytes::from_static(b"localhost")),
130        NameComponent::generic(Bytes::from_static(b"nfd")),
131        NameComponent::generic(Bytes::copy_from_slice(module)),
132        NameComponent::generic(Bytes::copy_from_slice(verb)),
133        NameComponent::generic(params_tlv),
134    ])
135}
136
137/// Build a dataset (status) name without parameters.
138///
139/// Result: `/localhost/nfd/<module>/<verb>`
140pub fn dataset_name(module: &[u8], verb: &[u8]) -> Name {
141    Name::from_components([
142        NameComponent::generic(Bytes::from_static(b"localhost")),
143        NameComponent::generic(Bytes::from_static(b"nfd")),
144        NameComponent::generic(Bytes::copy_from_slice(module)),
145        NameComponent::generic(Bytes::copy_from_slice(verb)),
146    ])
147}
148
149// ─── Name parser ─────────────────────────────────────────────────────────────
150
151/// Parsed management command extracted from an Interest name.
152#[derive(Debug)]
153pub struct ParsedCommand {
154    pub module: Bytes,
155    pub verb: Bytes,
156    pub params: Option<ControlParameters>,
157}
158
159/// Parse a management command from an Interest name.
160///
161/// Expects: `/localhost/nfd/<module>/<verb>[/<params>][/<signed-interest-components>]`
162///
163/// Returns `None` if the name doesn't match the management prefix or has
164/// too few components.
165pub fn parse_command_name(name: &Name) -> Option<ParsedCommand> {
166    let comps = name.components();
167    if comps.len() < 4 {
168        return None;
169    }
170
171    // Check /localhost/nfd prefix.
172    if comps[0].value.as_ref() != b"localhost" || comps[1].value.as_ref() != b"nfd" {
173        return None;
174    }
175
176    let module = comps[2].value.clone();
177    let verb = comps[3].value.clone();
178
179    // The 5th component (index 4), if present, is the ControlParameters TLV
180    // (full block including the 0x68 type byte, per NFD management spec).
181    // Components beyond index 4 (e.g. ParametersSha256DigestComponent from a
182    // signed Interest) are ignored — the router does not validate signatures.
183    let params = if comps.len() >= 5 {
184        ControlParameters::decode(comps[4].value.clone()).ok()
185    } else {
186        None
187    };
188
189    Some(ParsedCommand {
190        module,
191        verb,
192        params,
193    })
194}
195
196// ─── Tests ───────────────────────────────────────────────────────────────────
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn command_name_structure() {
204        let params = ControlParameters {
205            name: Some(Name::from_components([NameComponent::generic(
206                Bytes::from_static(b"test"),
207            )])),
208            cost: Some(10),
209            ..Default::default()
210        };
211        let name = command_name(module::RIB, verb::REGISTER, &params);
212        let comps = name.components();
213        assert_eq!(comps.len(), 5);
214        assert_eq!(comps[0].value.as_ref(), b"localhost");
215        assert_eq!(comps[1].value.as_ref(), b"nfd");
216        assert_eq!(comps[2].value.as_ref(), b"rib");
217        assert_eq!(comps[3].value.as_ref(), b"register");
218        // 5th component is the full ControlParameters TLV (type 0x68 + length + fields).
219        let decoded = ControlParameters::decode(comps[4].value.clone()).unwrap();
220        assert_eq!(decoded.cost, Some(10));
221    }
222
223    #[test]
224    fn dataset_name_structure() {
225        let name = dataset_name(module::FACES, verb::LIST);
226        let comps = name.components();
227        assert_eq!(comps.len(), 4);
228        assert_eq!(comps[2].value.as_ref(), b"faces");
229        assert_eq!(comps[3].value.as_ref(), b"list");
230    }
231
232    #[test]
233    fn parse_command_roundtrip() {
234        let params = ControlParameters {
235            uri: Some("shm://myapp".to_owned()),
236            ..Default::default()
237        };
238        let name = command_name(module::FACES, verb::CREATE, &params);
239        let parsed = parse_command_name(&name).unwrap();
240        assert_eq!(parsed.module.as_ref(), b"faces");
241        assert_eq!(parsed.verb.as_ref(), b"create");
242        let p = parsed.params.unwrap();
243        assert_eq!(p.uri.as_deref(), Some("shm://myapp"));
244    }
245
246    #[test]
247    fn parse_command_too_short() {
248        let name = Name::from_components([
249            NameComponent::generic(Bytes::from_static(b"localhost")),
250            NameComponent::generic(Bytes::from_static(b"nfd")),
251        ]);
252        assert!(parse_command_name(&name).is_none());
253    }
254
255    #[test]
256    fn parse_command_wrong_prefix() {
257        let name = Name::from_components([
258            NameComponent::generic(Bytes::from_static(b"localhop")),
259            NameComponent::generic(Bytes::from_static(b"nfd")),
260            NameComponent::generic(Bytes::from_static(b"rib")),
261            NameComponent::generic(Bytes::from_static(b"register")),
262        ]);
263        assert!(parse_command_name(&name).is_none());
264    }
265
266    #[test]
267    fn parse_command_no_params() {
268        let name = dataset_name(module::FACES, verb::LIST);
269        let parsed = parse_command_name(&name).unwrap();
270        assert_eq!(parsed.module.as_ref(), b"faces");
271        assert_eq!(parsed.verb.as_ref(), b"list");
272        assert!(parsed.params.is_none());
273    }
274}