Skip to main content

rovs_openflow/
multipart.rs

1//! OpenFlow Multipart messages (OF 1.3+).
2//!
3//! Multipart messages are used for statistics, table features, and other
4//! requests that may require multiple reply messages.
5
6use bytes::Bytes;
7
8use crate::flow::OFPP_ANY;
9use crate::instruction::InstructionList;
10use crate::message::{Message, MessageType};
11use crate::{Match, Version};
12
13/// Multipart message types (OF 1.3).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[repr(u16)]
16pub enum MultipartType {
17    /// Switch description
18    Desc = 0,
19    /// Individual flow statistics
20    Flow = 1,
21    /// Aggregate flow statistics
22    Aggregate = 2,
23    /// Flow table statistics
24    Table = 3,
25    /// Port statistics
26    PortStats = 4,
27    /// Queue statistics
28    Queue = 5,
29    /// Group statistics
30    Group = 6,
31    /// Group description
32    GroupDesc = 7,
33    /// Group features
34    GroupFeatures = 8,
35    /// Meter statistics
36    Meter = 9,
37    /// Meter configuration
38    MeterConfig = 10,
39    /// Meter features
40    MeterFeatures = 11,
41    /// Table features
42    TableFeatures = 12,
43    /// Port description
44    PortDesc = 13,
45    /// Experimenter extension
46    Experimenter = 0xffff,
47}
48
49impl TryFrom<u16> for MultipartType {
50    type Error = crate::Error;
51
52    fn try_from(v: u16) -> Result<Self, Self::Error> {
53        match v {
54            0 => Ok(Self::Desc),
55            1 => Ok(Self::Flow),
56            2 => Ok(Self::Aggregate),
57            3 => Ok(Self::Table),
58            4 => Ok(Self::PortStats),
59            5 => Ok(Self::Queue),
60            6 => Ok(Self::Group),
61            7 => Ok(Self::GroupDesc),
62            8 => Ok(Self::GroupFeatures),
63            9 => Ok(Self::Meter),
64            10 => Ok(Self::MeterConfig),
65            11 => Ok(Self::MeterFeatures),
66            12 => Ok(Self::TableFeatures),
67            13 => Ok(Self::PortDesc),
68            0xffff => Ok(Self::Experimenter),
69            _ => Err(crate::Error::Parse(format!(
70                "unknown multipart type: {v}"
71            ))),
72        }
73    }
74}
75
76/// Multipart request flags.
77pub mod multipart_flags {
78    /// More requests/replies to follow
79    pub const MORE: u16 = 1 << 0;
80}
81
82/// Multipart request header (8 bytes).
83///
84/// ```text
85/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
86/// |            type             |            flags              |
87/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
88/// |                           pad (4)                           |
89/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90/// ```
91#[derive(Debug, Clone)]
92pub struct MultipartHeader {
93    /// Multipart type
94    pub mp_type: MultipartType,
95    /// Flags (MORE = more messages follow)
96    pub flags: u16,
97}
98
99impl MultipartHeader {
100    /// Header size in bytes.
101    pub const SIZE: usize = 8;
102
103    /// Encode the header.
104    pub fn encode(&self) -> [u8; 8] {
105        let mut buf = [0u8; 8];
106        buf[0..2].copy_from_slice(&(self.mp_type as u16).to_be_bytes());
107        buf[2..4].copy_from_slice(&self.flags.to_be_bytes());
108        // bytes 4-7 are padding (zeros)
109        buf
110    }
111
112    /// Decode the header from bytes.
113    pub fn decode(data: &[u8]) -> crate::Result<Self> {
114        if data.len() < Self::SIZE {
115            return Err(crate::Error::Parse("multipart header too short".into()));
116        }
117
118        let mp_type = u16::from_be_bytes([data[0], data[1]]);
119        let flags = u16::from_be_bytes([data[2], data[3]]);
120
121        Ok(Self {
122            mp_type: MultipartType::try_from(mp_type)?,
123            flags,
124        })
125    }
126
127    /// Check if MORE flag is set.
128    pub fn has_more(&self) -> bool {
129        self.flags & multipart_flags::MORE != 0
130    }
131}
132
133/// Flow stats request body (follows multipart header).
134///
135/// ```text
136/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
137/// |   table_id  |   pad (3)                                      |
138/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
139/// |                          out_port                            |
140/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
141/// |                          out_group                           |
142/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
143/// |                           pad (4)                            |
144/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
145/// |                           cookie                             |
146/// |                                                               |
147/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
148/// |                        cookie_mask                           |
149/// |                                                               |
150/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
151/// |                       match (variable)                       |
152/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
153/// ```
154#[derive(Debug, Clone)]
155pub struct FlowStatsRequest {
156    /// Table ID (0xff for all tables)
157    pub table_id: u8,
158    /// Output port filter (OFPP_ANY for any)
159    pub out_port: u32,
160    /// Output group filter (OFPG_ANY for any)
161    pub out_group: u32,
162    /// Cookie filter
163    pub cookie: u64,
164    /// Cookie mask (0 to match all cookies)
165    pub cookie_mask: u64,
166    /// Match fields filter
167    pub match_fields: Match,
168}
169
170impl Default for FlowStatsRequest {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176impl FlowStatsRequest {
177    /// Create a new flow stats request that matches all flows.
178    pub fn new() -> Self {
179        Self {
180            table_id: 0xff,   // All tables
181            out_port: OFPP_ANY,
182            out_group: OFPP_ANY, // OFPG_ANY has same value
183            cookie: 0,
184            cookie_mask: 0,   // Match all cookies
185            match_fields: Match::new(),
186        }
187    }
188
189    /// Filter by table ID.
190    pub fn table(mut self, table_id: u8) -> Self {
191        self.table_id = table_id;
192        self
193    }
194
195    /// Filter by match fields.
196    pub fn match_fields(mut self, m: Match) -> Self {
197        self.match_fields = m;
198        self
199    }
200
201    /// Filter by cookie.
202    pub fn cookie(mut self, cookie: u64, mask: u64) -> Self {
203        self.cookie = cookie;
204        self.cookie_mask = mask;
205        self
206    }
207
208    /// Encode the flow stats request body.
209    pub fn encode(&self) -> Vec<u8> {
210        let match_bytes = self.match_fields.encode();
211        let mut buf = Vec::with_capacity(32 + match_bytes.len());
212
213        // table_id (1) + pad (3)
214        buf.push(self.table_id);
215        buf.extend([0u8; 3]);
216
217        // out_port (4)
218        buf.extend(self.out_port.to_be_bytes());
219
220        // out_group (4)
221        buf.extend(self.out_group.to_be_bytes());
222
223        // pad (4)
224        buf.extend([0u8; 4]);
225
226        // cookie (8)
227        buf.extend(self.cookie.to_be_bytes());
228
229        // cookie_mask (8)
230        buf.extend(self.cookie_mask.to_be_bytes());
231
232        // match (variable)
233        buf.extend(match_bytes);
234
235        buf
236    }
237
238    /// Create the complete multipart request message.
239    pub fn to_message(&self, version: Version, xid: u32) -> Message {
240        let header = MultipartHeader {
241            mp_type: MultipartType::Flow,
242            flags: 0,
243        };
244
245        let mut body = Vec::new();
246        body.extend(header.encode());
247        body.extend(self.encode());
248
249        Message::new(version, MessageType::MultipartRequest, xid, Bytes::from(body))
250    }
251}
252
253/// Individual flow statistics from a FlowStats reply.
254///
255/// ```text
256/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
257/// |           length            |   table_id  |      pad        |
258/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
259/// |                         duration_sec                         |
260/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
261/// |                        duration_nsec                         |
262/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
263/// |           priority          |         idle_timeout           |
264/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
265/// |         hard_timeout        |            flags               |
266/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
267/// |                           pad (4)                            |
268/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
269/// |                           cookie                             |
270/// |                                                               |
271/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
272/// |                        packet_count                          |
273/// |                                                               |
274/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
275/// |                         byte_count                           |
276/// |                                                               |
277/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
278/// |                       match (variable)                       |
279/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
280/// |                   instructions (variable)                    |
281/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
282/// ```
283#[derive(Debug, Clone)]
284pub struct FlowStatsEntry {
285    /// Table ID
286    pub table_id: u8,
287    /// Time flow has been alive (seconds)
288    pub duration_sec: u32,
289    /// Time flow has been alive (nanoseconds beyond duration_sec)
290    pub duration_nsec: u32,
291    /// Priority
292    pub priority: u16,
293    /// Idle timeout
294    pub idle_timeout: u16,
295    /// Hard timeout
296    pub hard_timeout: u16,
297    /// Flags
298    pub flags: u16,
299    /// Cookie
300    pub cookie: u64,
301    /// Packet count
302    pub packet_count: u64,
303    /// Byte count
304    pub byte_count: u64,
305    /// Match fields
306    pub match_fields: Match,
307    /// Instructions (raw bytes for now)
308    pub instructions: Vec<u8>,
309}
310
311impl FlowStatsEntry {
312    /// Fixed header size before match (48 bytes).
313    pub const FIXED_SIZE: usize = 48;
314
315    /// Parse a single flow stats entry from bytes.
316    #[allow(clippy::similar_names)]
317    pub fn decode(data: &[u8]) -> crate::Result<(Self, usize)> {
318        if data.len() < Self::FIXED_SIZE {
319            return Err(crate::Error::Parse("flow stats entry too short".into()));
320        }
321
322        let length = u16::from_be_bytes([data[0], data[1]]) as usize;
323        if data.len() < length {
324            return Err(crate::Error::Parse("flow stats entry truncated".into()));
325        }
326
327        let table_id = data[2];
328        // data[3] is padding
329        let duration_sec = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
330        let duration_nsec = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
331        let priority = u16::from_be_bytes([data[12], data[13]]);
332        let idle_timeout = u16::from_be_bytes([data[14], data[15]]);
333        let hard_timeout = u16::from_be_bytes([data[16], data[17]]);
334        let flags = u16::from_be_bytes([data[18], data[19]]);
335        // data[20..24] is padding
336        let cookie = u64::from_be_bytes([
337            data[24], data[25], data[26], data[27],
338            data[28], data[29], data[30], data[31],
339        ]);
340        let packet_count = u64::from_be_bytes([
341            data[32], data[33], data[34], data[35],
342            data[36], data[37], data[38], data[39],
343        ]);
344        let byte_count = u64::from_be_bytes([
345            data[40], data[41], data[42], data[43],
346            data[44], data[45], data[46], data[47],
347        ]);
348
349        // Parse match
350        let match_data = &data[Self::FIXED_SIZE..];
351        let (match_fields, match_len) = Match::decode(match_data)?;
352
353        // Instructions follow the match
354        let instructions_start = Self::FIXED_SIZE + match_len;
355        let instructions = data[instructions_start..length].to_vec();
356
357        Ok((
358            Self {
359                table_id,
360                duration_sec,
361                duration_nsec,
362                priority,
363                idle_timeout,
364                hard_timeout,
365                flags,
366                cookie,
367                packet_count,
368                byte_count,
369                match_fields,
370                instructions,
371            },
372            length,
373        ))
374    }
375
376    /// Decode the instructions from the raw bytes.
377    ///
378    /// Returns the decoded instruction list. This is a separate method
379    /// because decoding instructions may fail and is not always needed.
380    pub fn decoded_instructions(&self) -> crate::Result<InstructionList> {
381        InstructionList::decode(&self.instructions)
382    }
383}
384
385/// Parse all flow stats entries from a multipart reply body.
386pub fn parse_flow_stats_reply(body: &[u8]) -> crate::Result<(Vec<FlowStatsEntry>, bool)> {
387    if body.len() < MultipartHeader::SIZE {
388        return Err(crate::Error::Parse("multipart reply too short".into()));
389    }
390
391    let header = MultipartHeader::decode(body)?;
392    if header.mp_type != MultipartType::Flow {
393        return Err(crate::Error::Parse(format!(
394            "expected Flow multipart type, got {:?}",
395            header.mp_type
396        )));
397    }
398
399    let mut entries = Vec::new();
400    let mut offset = MultipartHeader::SIZE;
401
402    while offset < body.len() {
403        let remaining = &body[offset..];
404        if remaining.len() < 4 {
405            break; // Not enough data for another entry
406        }
407
408        let (entry, consumed) = FlowStatsEntry::decode(remaining)?;
409        entries.push(entry);
410        offset += consumed;
411    }
412
413    Ok((entries, header.has_more()))
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    #[test]
421    fn multipart_type_values() {
422        assert_eq!(MultipartType::Desc as u16, 0);
423        assert_eq!(MultipartType::Flow as u16, 1);
424        assert_eq!(MultipartType::Aggregate as u16, 2);
425        assert_eq!(MultipartType::Table as u16, 3);
426        assert_eq!(MultipartType::PortStats as u16, 4);
427    }
428
429    #[test]
430    fn multipart_header_encode() {
431        let header = MultipartHeader {
432            mp_type: MultipartType::Flow,
433            flags: 0,
434        };
435        let bytes = header.encode();
436        assert_eq!(bytes.len(), 8);
437        assert_eq!(bytes[0..2], [0x00, 0x01]); // Flow = 1
438        assert_eq!(bytes[2..4], [0x00, 0x00]); // flags = 0
439        assert_eq!(bytes[4..8], [0x00, 0x00, 0x00, 0x00]); // padding
440    }
441
442    #[test]
443    fn multipart_header_decode() {
444        let data = [0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
445        let header = MultipartHeader::decode(&data).unwrap();
446        assert_eq!(header.mp_type, MultipartType::Flow);
447        assert!(header.has_more());
448    }
449
450    #[test]
451    fn flow_stats_request_default() {
452        let req = FlowStatsRequest::new();
453        assert_eq!(req.table_id, 0xff);
454        assert_eq!(req.out_port, OFPP_ANY);
455        assert_eq!(req.cookie, 0);
456        assert_eq!(req.cookie_mask, 0);
457    }
458
459    #[test]
460    fn flow_stats_request_encode() {
461        let req = FlowStatsRequest::new().table(0);
462        let bytes = req.encode();
463
464        // 32 bytes fixed + 8 bytes empty match (type=1, len=4, pad=4)
465        assert!(bytes.len() >= 32);
466        assert_eq!(bytes[0], 0); // table_id
467    }
468
469    #[test]
470    fn flow_stats_request_to_message() {
471        let req = FlowStatsRequest::new();
472        let msg = req.to_message(Version::Of13, 42);
473
474        assert_eq!(msg.header.msg_type, MessageType::MultipartRequest);
475        assert_eq!(msg.header.xid, 42);
476        // Body should have multipart header (8) + request body (32+)
477        assert!(msg.body.len() >= 40);
478    }
479
480    #[test]
481    fn flow_stats_entry_decode() {
482        // Minimal flow stats entry: 48 fixed + 8 empty match = 56 bytes
483        let mut data = vec![0u8; 56];
484
485        // length = 56
486        data[0] = 0x00;
487        data[1] = 0x38;
488
489        // table_id = 0
490        data[2] = 0x00;
491
492        // priority = 100
493        data[12] = 0x00;
494        data[13] = 0x64;
495
496        // cookie = 0x1234
497        data[24] = 0x00;
498        data[25] = 0x00;
499        data[26] = 0x00;
500        data[27] = 0x00;
501        data[28] = 0x00;
502        data[29] = 0x00;
503        data[30] = 0x12;
504        data[31] = 0x34;
505
506        // packet_count = 1000
507        data[32..40].copy_from_slice(&1000u64.to_be_bytes());
508
509        // byte_count = 64000
510        data[40..48].copy_from_slice(&64000u64.to_be_bytes());
511
512        // Match: type=1 (OXM), length=4, no fields, 4 bytes padding
513        data[48] = 0x00;
514        data[49] = 0x01;
515        data[50] = 0x00;
516        data[51] = 0x04;
517        // padding
518        data[52..56].copy_from_slice(&[0u8; 4]);
519
520        let (entry, consumed) = FlowStatsEntry::decode(&data).unwrap();
521
522        assert_eq!(consumed, 56);
523        assert_eq!(entry.table_id, 0);
524        assert_eq!(entry.priority, 100);
525        assert_eq!(entry.cookie, 0x1234);
526        assert_eq!(entry.packet_count, 1000);
527        assert_eq!(entry.byte_count, 64000);
528    }
529}