Skip to main content

netget/server/gnmi/
semantic.rs

1//! Validate the selected gNMI RPC semantics, independently of protobuf framing.
2use super::{
3    proto::gnmi as pb,
4    value::{self, Notification, Path, Update},
5};
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8use tonic::Status;
9pub fn check_model(action: &Value) -> Result<(), Status> {
10    fn walk(v: &Value, depth: usize, nodes: &mut usize, bytes: &mut usize) -> Result<(), Status> {
11        *nodes += 1;
12        *bytes += 64;
13        if depth > 32 || *nodes > 10_000 || *bytes > 1024 * 1024 {
14            return Err(Status::resource_exhausted(
15                "gNMI model depth/node/retained bound",
16            ));
17        }
18        match v {
19            Value::String(s) => {
20                *bytes += s.len();
21                if s.len() > 65536 || *bytes > 1024 * 1024 {
22                    return Err(Status::resource_exhausted("gNMI model text/retained bound"));
23                }
24            }
25            Value::Array(v) => {
26                for value in v {
27                    walk(value, depth + 1, nodes, bytes)?;
28                }
29            }
30            Value::Object(v) => {
31                for (key, value) in v {
32                    if key.len() > 256 {
33                        return Err(Status::resource_exhausted("gNMI model key bound"));
34                    }
35                    *bytes += key.len();
36                    walk(value, depth + 1, nodes, bytes)?;
37                }
38            }
39            _ => {}
40        }
41        Ok(())
42    }
43    walk(action, 0, &mut 0, &mut 0)
44}
45#[derive(Clone, Debug, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct Model {
48    pub name: String,
49    #[serde(default)]
50    pub organization: String,
51    #[serde(default)]
52    pub version: String,
53}
54pub fn models(values: Vec<pb::ModelData>) -> Result<Vec<Model>, Status> {
55    let models: Vec<_> = values
56        .into_iter()
57        .map(|v| Model {
58            name: v.name,
59            organization: v.organization,
60            version: v.version,
61        })
62        .collect();
63    model_proto(models.clone())?;
64    Ok(models)
65}
66pub fn model_proto(values: Vec<Model>) -> Result<Vec<pb::ModelData>, Status> {
67    if values.len() > 128 {
68        return Err(Status::resource_exhausted("model count exceeds 128"));
69    }
70    values
71        .into_iter()
72        .map(|v| {
73            if v.name.is_empty()
74                || [&v.name, &v.organization, &v.version]
75                    .iter()
76                    .any(|s| s.len() > 256 || s.contains('\0'))
77            {
78                return Err(Status::invalid_argument("invalid model name/text"));
79            }
80            Ok(pb::ModelData {
81                name: v.name,
82                organization: v.organization,
83                version: v.version,
84            })
85        })
86        .collect()
87}
88pub fn get_request(request: &pb::GetRequest) -> Result<Value, Status> {
89    value::encoding_name(request.encoding)?;
90    let kind = match request.r#type {
91        0 => "ALL",
92        1 => "CONFIG",
93        2 => "STATE",
94        3 => "OPERATIONAL",
95        _ => return Err(Status::invalid_argument("unknown Get data type")),
96    };
97    if request.path.len() > 128 || !request.extension.is_empty() {
98        return Err(Status::unimplemented(
99            "Get paths/extensions outside selected scope",
100        ));
101    }
102    Ok(
103        json!({"prefix":Path::from_proto(request.prefix.clone())?,"path":request.path.iter().cloned().map(|p|Path::from_proto(Some(p))).collect::<Result<Vec<_>,_>>()?,"data_type":kind,"encoding":value::encoding_name(request.encoding)?,"use_models":models(request.use_models.clone())?}),
104    )
105}
106pub fn set_request(request: &pb::SetRequest) -> Result<Value, Status> {
107    if !request.union_replace.is_empty() || !request.extension.is_empty() {
108        return Err(Status::unimplemented(
109            "union_replace and extensions are excluded",
110        ));
111    }
112    if request.delete.len() + request.replace.len() + request.update.len() > 256 {
113        return Err(Status::resource_exhausted("Set operations exceed 256"));
114    }
115    let convert = |values: &[pb::Update]| {
116        values
117            .iter()
118            .cloned()
119            .map(Update::from_proto)
120            .collect::<Result<Vec<_>, _>>()
121    };
122    Ok(
123        json!({"prefix":Path::from_proto(request.prefix.clone())?,"delete":request.delete.iter().cloned().map(|p|Path::from_proto(Some(p))).collect::<Result<Vec<_>,_>>()?,"replace":convert(&request.replace)?,"update":convert(&request.update)?}),
124    )
125}
126pub fn set_response(request: &pb::SetRequest, timestamp: &str) -> Result<pb::SetResponse, Status> {
127    set_request(request)?;
128    let mut results = Vec::new();
129    for (paths, operation) in [
130        (request.delete.clone(), 1),
131        (
132            request
133                .replace
134                .iter()
135                .map(|v| v.path.clone().unwrap_or_default())
136                .collect(),
137            2,
138        ),
139        (
140            request
141                .update
142                .iter()
143                .map(|v| v.path.clone().unwrap_or_default())
144                .collect(),
145            3,
146        ),
147    ] {
148        for path in paths {
149            results.push(pb::UpdateResult {
150                timestamp: 0,
151                path: Some(path),
152                message: None,
153                op: operation,
154            });
155        }
156    }
157    value::checked(pb::SetResponse {
158        prefix: request.prefix.clone(),
159        response: results,
160        message: None,
161        timestamp: timestamp.parse().map_err(|_| {
162            Status::invalid_argument("timestamp must be i64 nanosecond decimal string")
163        })?,
164        extension: vec![],
165    })
166}
167pub fn set_result(response: pb::SetResponse) -> Result<Value, Status> {
168    if response.message.is_some() || !response.extension.is_empty() {
169        return Err(Status::unimplemented(
170            "legacy errors/extensions are excluded",
171        ));
172    }
173    let result = response
174        .response
175        .into_iter()
176        .map(|r| {
177            if r.message.is_some() {
178                return Err(Status::unimplemented("legacy update errors are excluded"));
179            }
180            let operation = match r.op {
181                1 => "DELETE",
182                2 => "REPLACE",
183                3 => "UPDATE",
184                _ => {
185                    return Err(Status::invalid_argument(
186                        "unsupported Set acknowledgement operation",
187                    ))
188                }
189            };
190            Ok(json!({"path":Path::from_proto(r.path)?,"operation":operation}))
191        })
192        .collect::<Result<Vec<_>, _>>()?;
193    Ok(
194        json!({"prefix":Path::from_proto(response.prefix)?,"timestamp":response.timestamp.to_string(),"response":result}),
195    )
196}
197pub fn check_set_ack(request: &pb::SetRequest, response: &pb::SetResponse) -> Result<(), Status> {
198    let expected = set_response(request, "0")?;
199    if Path::from_proto(response.prefix.clone())? != Path::from_proto(expected.prefix)?
200        || response.response.len() != expected.response.len()
201    {
202        return Err(Status::invalid_argument(
203            "Set response does not acknowledge the requested transaction",
204        ));
205    }
206    for (actual, expected) in response.response.iter().zip(expected.response) {
207        if actual.op != expected.op
208            || Path::from_proto(actual.path.clone())? != Path::from_proto(expected.path)?
209        {
210            return Err(Status::invalid_argument(
211                "Set response does not acknowledge the requested transaction",
212            ));
213        }
214    }
215    Ok(())
216}
217pub fn get_result(response: pb::GetResponse, encoding: i32) -> Result<Value, Status> {
218    if response.error.is_some() || !response.extension.is_empty() {
219        return Err(Status::unimplemented(
220            "legacy errors/extensions are excluded",
221        ));
222    }
223    for n in &response.notification {
224        value::notification_encoding(n, encoding)?;
225    }
226    Ok(
227        json!({"notification":response.notification.into_iter().map(Notification::from_proto).collect::<Result<Vec<_>,_>>()?}),
228    )
229}
230pub fn capability_result(response: pb::CapabilityResponse) -> Result<Value, Status> {
231    if !response.extension.is_empty() {
232        return Err(Status::unimplemented("capability extensions are excluded"));
233    }
234    if response.g_nmi_version.len() > 256 {
235        return Err(Status::resource_exhausted("gNMI version bound"));
236    }
237    let encodings = response
238        .supported_encodings
239        .into_iter()
240        .map(value::encoding_name)
241        .collect::<Result<Vec<_>, _>>()?;
242    Ok(
243        json!({"supported_models":models(response.supported_models)?,"supported_encodings":encodings,"gnmi_version":response.g_nmi_version}),
244    )
245}
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub enum Mode {
248    Once,
249    Poll,
250    Stream,
251}
252#[derive(Clone)]
253pub struct Subscription {
254    pub mode: Mode,
255    pub encoding: i32,
256    pub updates_only: bool,
257    pub event: Value,
258}
259pub fn subscription(list: &pb::SubscriptionList) -> Result<Subscription, Status> {
260    value::encoding_name(list.encoding)?;
261    // gNMIc explicitly encodes marking0 when --qos0 disables marking. It is
262    // the ordinary default DSCP behavior; nonzero marking is outside this subset.
263    if list.qos.as_ref().is_some_and(|q| q.marking != 0) || list.allow_aggregation {
264        return Err(Status::unimplemented(
265            "nonzero QoS and aggregation are excluded",
266        ));
267    }
268    if list.subscription.is_empty() || list.subscription.len() > 128 {
269        return Err(Status::invalid_argument(
270            "subscription requires 1..128 paths",
271        ));
272    }
273    let mode = match list.mode {
274        0 => Mode::Stream,
275        1 => Mode::Once,
276        2 => Mode::Poll,
277        _ => return Err(Status::invalid_argument("unknown subscription list mode")),
278    };
279    let paths=list.subscription.iter().map(|s|{
280        if !(0..=1).contains(&s.mode)||s.sample_interval!=0||s.suppress_redundant||s.heartbeat_interval!=0{return Err(Status::unimplemented("SAMPLE, suppression and heartbeat are excluded"));}
281        Ok(json!({"path":Path::from_proto(s.path.clone())?,"mode":if s.mode==0{"TARGET_DEFINED"}else{"ON_CHANGE"}}))
282    }).collect::<Result<Vec<_>,_>>()?;
283    let event = json!({"prefix":Path::from_proto(list.prefix.clone())?,"subscription":paths,"mode":match mode{Mode::Once=>"ONCE",Mode::Poll=>"POLL",Mode::Stream=>"STREAM"},"encoding":value::encoding_name(list.encoding)?,"updates_only":list.updates_only,"use_models":models(list.use_models.clone())?});
284    Ok(Subscription {
285        mode,
286        encoding: list.encoding,
287        updates_only: list.updates_only,
288        event,
289    })
290}
291pub enum Answer {
292    Capabilities(pb::CapabilityResponse),
293    Get(Vec<pb::Notification>),
294    Set(String),
295    Update(Vec<pb::Notification>),
296    Sync,
297    Wait(u64),
298    Finish,
299    Error(Status),
300}
301fn decode<T: serde::de::DeserializeOwned>(v: &Value) -> Result<T, Status> {
302    serde_json::from_value(v.clone())
303        .map_err(|_| Status::invalid_argument("invalid typed gNMI action"))
304}
305fn notifications(v: &Value) -> Result<Vec<pb::Notification>, Status> {
306    let values = v
307        .as_array()
308        .ok_or_else(|| Status::invalid_argument("notification must be an array"))?;
309    if values.len() > 128 {
310        return Err(Status::resource_exhausted("notification count exceeds 128"));
311    }
312    values
313        .iter()
314        .map(|v| decode::<Notification>(v)?.into_proto())
315        .collect()
316}
317pub fn answer(action: &Value) -> Result<Answer, Status> {
318    Ok(match action["type"].as_str().unwrap_or("") {
319        "gnmi_capabilities" => {
320            let model_values = action["supported_models"]
321                .as_array()
322                .ok_or_else(|| Status::invalid_argument("supported_models must be an array"))?;
323            if model_values.len() > 128 {
324                return Err(Status::resource_exhausted("model count exceeds 128"));
325            }
326            let model = model_proto(decode(&action["supported_models"])?)?;
327            let encodings = action["supported_encodings"]
328                .as_array()
329                .ok_or_else(|| Status::invalid_argument("supported_encodings must be an array"))?;
330            if encodings.is_empty() || encodings.len() > 4 {
331                return Err(Status::invalid_argument(
332                    "advertise 1..4 supported encodings",
333                ));
334            }
335            let encodings = encodings
336                .iter()
337                .map(|v| value::encoding(v.as_str().unwrap_or("")))
338                .collect::<Result<Vec<_>, _>>()?;
339            let response = pb::CapabilityResponse {
340                supported_models: model,
341                supported_encodings: encodings,
342                g_nmi_version: "0.10.0".into(),
343                extension: vec![],
344            };
345            Answer::Capabilities(value::checked(response)?)
346        }
347        "gnmi_get_response" => {
348            let n = notifications(&action["notification"])?;
349            value::checked(pb::GetResponse {
350                notification: n.clone(),
351                error: None,
352                extension: vec![],
353            })?;
354            Answer::Get(n)
355        }
356        "gnmi_set_accepted" => {
357            let t = action["timestamp"]
358                .as_str()
359                .ok_or_else(|| Status::invalid_argument("timestamp required"))?;
360            t.parse::<i64>()
361                .map_err(|_| Status::invalid_argument("invalid timestamp"))?;
362            Answer::Set(t.into())
363        }
364        "gnmi_update" => {
365            let n = notifications(&action["notification"])?;
366            if n.len() > 16 {
367                return Err(Status::resource_exhausted(
368                    "at most 16 queued notifications",
369                ));
370            }
371            let mut bytes = 0usize;
372            for v in &n {
373                let response = value::checked(pb::SubscribeResponse {
374                    response: Some(pb::subscribe_response::Response::Update(v.clone())),
375                    extension: vec![],
376                })?;
377                bytes += prost::Message::encoded_len(&response);
378                if bytes > super::codec::MAX_MESSAGE_BYTES {
379                    return Err(Status::resource_exhausted(
380                        "pending notifications exceed 1 MiB",
381                    ));
382                }
383            }
384            Answer::Update(n)
385        }
386        "gnmi_sync" => Answer::Sync,
387        "gnmi_wait" => {
388            let milliseconds = action["milliseconds"]
389                .as_u64()
390                .filter(|v| (1..=1000).contains(v))
391                .ok_or_else(|| Status::invalid_argument("wait milliseconds must be 1..1000"))?;
392            Answer::Wait(milliseconds)
393        }
394        "gnmi_finish" => Answer::Finish,
395        "gnmi_error" => {
396            let code = action["code"]
397                .as_i64()
398                .filter(|v| (1..=16).contains(v))
399                .ok_or_else(|| Status::invalid_argument("error code must be 1..16"))?;
400            let message = action["message"]
401                .as_str()
402                .ok_or_else(|| Status::invalid_argument("error message required"))?;
403            if message.len() > 512 {
404                return Err(Status::resource_exhausted(
405                    "error diagnostic exceeds 512 bytes",
406                ));
407            }
408            Answer::Error(Status::new(tonic::Code::from_i32(code as i32), message))
409        }
410        _ => return Err(Status::invalid_argument("unknown gNMI server action")),
411    })
412}