Skip to main content

rovs_openflow/
flow.rs

1//! OpenFlow flow entries and modifications.
2
3use bytes::Bytes;
4
5use crate::instruction::{Instruction, InstructionList};
6use crate::message::{Message, MessageType};
7use crate::{ActionList, Match, Version};
8
9/// Flow statistics read from the switch.
10#[derive(Debug, Clone)]
11pub struct FlowStats {
12    /// Table ID
13    pub table_id: u8,
14    /// Priority (higher = more specific)
15    pub priority: u16,
16    /// Cookie (opaque identifier)
17    pub cookie: u64,
18    /// Match fields
19    pub match_fields: Match,
20    /// Actions to apply
21    pub actions: ActionList,
22    /// Idle timeout (seconds, 0 = no timeout)
23    pub idle_timeout: u16,
24    /// Hard timeout (seconds, 0 = no timeout)
25    pub hard_timeout: u16,
26    /// Packet count
27    pub packet_count: u64,
28    /// Byte count
29    pub byte_count: u64,
30}
31
32/// Flow command type.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(u8)]
35pub enum FlowCommand {
36    /// Add a new flow
37    Add = 0,
38    /// Modify matching flows
39    Modify = 1,
40    /// Modify matching flows (strict match)
41    ModifyStrict = 2,
42    /// Delete matching flows
43    Delete = 3,
44    /// Delete matching flows (strict match)
45    DeleteStrict = 4,
46}
47
48/// Flow flags.
49#[derive(Debug, Clone, Copy, Default)]
50pub struct FlowFlags {
51    /// Send flow removed message
52    pub send_flow_rem: bool,
53    /// Check for overlapping entries
54    pub check_overlap: bool,
55    /// Reset flow counters
56    pub reset_counts: bool,
57    /// Don't keep track of packet count
58    pub no_pkt_counts: bool,
59    /// Don't keep track of byte count
60    pub no_byte_counts: bool,
61}
62
63/// Flow flag bit values (OF 1.3).
64pub mod flow_flags {
65    /// Send flow removed message when flow expires
66    pub const SEND_FLOW_REM: u16 = 1 << 0;
67    /// Check for overlapping entries first
68    pub const CHECK_OVERLAP: u16 = 1 << 1;
69    /// Reset flow packet and byte counts
70    pub const RESET_COUNTS: u16 = 1 << 2;
71    /// Don't keep track of packet count
72    pub const NO_PKT_COUNTS: u16 = 1 << 3;
73    /// Don't keep track of byte count
74    pub const NO_BYT_COUNTS: u16 = 1 << 4;
75}
76
77impl FlowFlags {
78    /// Convert flags to wire format.
79    pub fn to_wire(self) -> u16 {
80        let mut flags = 0u16;
81        if self.send_flow_rem {
82            flags |= flow_flags::SEND_FLOW_REM;
83        }
84        if self.check_overlap {
85            flags |= flow_flags::CHECK_OVERLAP;
86        }
87        if self.reset_counts {
88            flags |= flow_flags::RESET_COUNTS;
89        }
90        if self.no_pkt_counts {
91            flags |= flow_flags::NO_PKT_COUNTS;
92        }
93        if self.no_byte_counts {
94            flags |= flow_flags::NO_BYT_COUNTS;
95        }
96        flags
97    }
98}
99
100/// Special port value for "any" (used in delete commands).
101pub const OFPP_ANY: u32 = 0xffff_ffff;
102/// Special group value for "any" (used in delete commands).
103pub const OFPG_ANY: u32 = 0xffff_ffff;
104/// Special buffer ID for "no buffer".
105pub const OFP_NO_BUFFER: u32 = 0xffff_ffff;
106
107/// A flow entry to add, modify, or delete.
108#[derive(Debug, Clone)]
109pub struct Flow {
110    /// Command (add, modify, delete)
111    pub command: FlowCommand,
112    /// Table ID (0xff for all tables on delete)
113    pub table_id: u8,
114    /// Priority
115    pub priority: u16,
116    /// Cookie
117    pub cookie: u64,
118    /// Cookie mask (for modify/delete)
119    pub cookie_mask: u64,
120    /// Match fields
121    pub match_fields: Match,
122    /// Actions (wrapped in ApplyActions instruction if instructions is empty)
123    pub actions: ActionList,
124    /// Instructions (OF 1.3+, takes precedence over actions if set)
125    pub instructions: InstructionList,
126    /// Idle timeout
127    pub idle_timeout: u16,
128    /// Hard timeout
129    pub hard_timeout: u16,
130    /// Flags
131    pub flags: FlowFlags,
132    /// Output port (for delete commands)
133    pub out_port: Option<u32>,
134    /// Output group (for delete commands)
135    pub out_group: Option<u32>,
136    /// Buffer ID (for packet-out)
137    pub buffer_id: Option<u32>,
138}
139
140impl Flow {
141    /// Create a new flow add command.
142    pub fn add() -> Self {
143        Self {
144            command: FlowCommand::Add,
145            table_id: 0,
146            priority: 0,
147            cookie: 0,
148            cookie_mask: 0,
149            match_fields: Match::new(),
150            actions: ActionList::new(),
151            instructions: InstructionList::new(),
152            idle_timeout: 0,
153            hard_timeout: 0,
154            flags: FlowFlags::default(),
155            out_port: None,
156            out_group: None,
157            buffer_id: None,
158        }
159    }
160
161    /// Create a new flow delete command.
162    pub fn delete() -> Self {
163        Self {
164            command: FlowCommand::Delete,
165            table_id: 0xff, // All tables
166            priority: 0,
167            cookie: 0,
168            cookie_mask: 0,
169            match_fields: Match::new(),
170            actions: ActionList::new(),
171            instructions: InstructionList::new(),
172            idle_timeout: 0,
173            hard_timeout: 0,
174            flags: FlowFlags::default(),
175            out_port: None,
176            out_group: None,
177            buffer_id: None,
178        }
179    }
180
181    /// Set the table ID.
182    pub fn table(mut self, id: u8) -> Self {
183        self.table_id = id;
184        self
185    }
186
187    /// Set the priority.
188    pub fn priority(mut self, priority: u16) -> Self {
189        self.priority = priority;
190        self
191    }
192
193    /// Set the cookie.
194    pub fn cookie(mut self, cookie: u64) -> Self {
195        self.cookie = cookie;
196        self
197    }
198
199    /// Set the match fields.
200    pub fn match_fields(mut self, m: Match) -> Self {
201        self.match_fields = m;
202        self
203    }
204
205    /// Set the actions (will be wrapped in ApplyActions instruction).
206    pub fn actions(mut self, actions: ActionList) -> Self {
207        self.actions = actions;
208        self
209    }
210
211    /// Set the instructions directly (OF 1.3+).
212    pub fn instructions(mut self, instructions: InstructionList) -> Self {
213        self.instructions = instructions;
214        self
215    }
216
217    /// Set the idle timeout.
218    pub fn idle_timeout(mut self, timeout: u16) -> Self {
219        self.idle_timeout = timeout;
220        self
221    }
222
223    /// Set the hard timeout.
224    pub fn hard_timeout(mut self, timeout: u16) -> Self {
225        self.hard_timeout = timeout;
226        self
227    }
228
229    /// Encode the FlowMod fixed fields (40 bytes).
230    ///
231    /// ```text
232    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
233    /// |                            cookie                             |
234    /// |                                                               |
235    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
236    /// |                         cookie_mask                           |
237    /// |                                                               |
238    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
239    /// |   table_id  |    command    |         idle_timeout            |
240    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
241    /// |         hard_timeout        |           priority              |
242    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
243    /// |                          buffer_id                            |
244    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
245    /// |                          out_port                             |
246    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
247    /// |                          out_group                            |
248    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
249    /// |            flags            |           pad (zeros)           |
250    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
251    /// ```
252    fn encode_fixed(&self) -> [u8; 40] {
253        let mut buf = [0u8; 40];
254
255        // cookie (8 bytes)
256        buf[0..8].copy_from_slice(&self.cookie.to_be_bytes());
257
258        // cookie_mask (8 bytes)
259        buf[8..16].copy_from_slice(&self.cookie_mask.to_be_bytes());
260
261        // table_id (1 byte)
262        buf[16] = self.table_id;
263
264        // command (1 byte)
265        buf[17] = self.command as u8;
266
267        // idle_timeout (2 bytes)
268        buf[18..20].copy_from_slice(&self.idle_timeout.to_be_bytes());
269
270        // hard_timeout (2 bytes)
271        buf[20..22].copy_from_slice(&self.hard_timeout.to_be_bytes());
272
273        // priority (2 bytes)
274        buf[22..24].copy_from_slice(&self.priority.to_be_bytes());
275
276        // buffer_id (4 bytes)
277        let buffer_id = self.buffer_id.unwrap_or(OFP_NO_BUFFER);
278        buf[24..28].copy_from_slice(&buffer_id.to_be_bytes());
279
280        // out_port (4 bytes)
281        let out_port = self.out_port.unwrap_or(OFPP_ANY);
282        buf[28..32].copy_from_slice(&out_port.to_be_bytes());
283
284        // out_group (4 bytes)
285        let out_group = self.out_group.unwrap_or(OFPG_ANY);
286        buf[32..36].copy_from_slice(&out_group.to_be_bytes());
287
288        // flags (2 bytes)
289        buf[36..38].copy_from_slice(&self.flags.to_wire().to_be_bytes());
290
291        // pad (2 bytes) - already zeroed
292
293        buf
294    }
295
296    /// Encode the complete FlowMod body (without OF header).
297    ///
298    /// Returns: fixed fields (40) + match (variable) + instructions (variable)
299    pub fn encode(&self) -> Vec<u8> {
300        let fixed = self.encode_fixed();
301        let match_bytes = self.match_fields.encode();
302
303        // Build instructions: use explicit instructions if set, otherwise wrap actions
304        let instruction_bytes = if self.instructions.is_empty() && !self.actions.is_empty() {
305            // Wrap actions in ApplyActions instruction
306            let inst = Instruction::ApplyActions(self.actions.clone());
307            inst.encode()
308        } else {
309            self.instructions.encode()
310        };
311
312        let mut buf = Vec::with_capacity(40 + match_bytes.len() + instruction_bytes.len());
313        buf.extend_from_slice(&fixed);
314        buf.extend(match_bytes);
315        buf.extend(instruction_bytes);
316        buf
317    }
318
319    /// Create a complete OpenFlow message for this FlowMod.
320    ///
321    /// # Arguments
322    /// * `version` - OpenFlow version (should be 1.3+)
323    /// * `xid` - Transaction ID
324    pub fn to_message(&self, version: Version, xid: u32) -> Message {
325        let body = self.encode();
326        Message::new(version, MessageType::FlowMod, xid, Bytes::from(body))
327    }
328}
329
330// ============================================================================
331// Tests
332// ============================================================================
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::action::OutputPort;
338
339    #[test]
340    fn flow_flags_to_wire_empty() {
341        let flags = FlowFlags::default();
342        assert_eq!(flags.to_wire(), 0);
343    }
344
345    #[test]
346    fn flow_flags_to_wire_all() {
347        let flags = FlowFlags {
348            send_flow_rem: true,
349            check_overlap: true,
350            reset_counts: true,
351            no_pkt_counts: true,
352            no_byte_counts: true,
353        };
354        assert_eq!(flags.to_wire(), 0b11111);
355    }
356
357    #[test]
358    fn flow_flags_to_wire_send_flow_rem() {
359        let flags = FlowFlags {
360            send_flow_rem: true,
361            ..Default::default()
362        };
363        assert_eq!(flags.to_wire(), flow_flags::SEND_FLOW_REM);
364    }
365
366    #[test]
367    fn flow_command_wire_values() {
368        assert_eq!(FlowCommand::Add as u8, 0);
369        assert_eq!(FlowCommand::Modify as u8, 1);
370        assert_eq!(FlowCommand::ModifyStrict as u8, 2);
371        assert_eq!(FlowCommand::Delete as u8, 3);
372        assert_eq!(FlowCommand::DeleteStrict as u8, 4);
373    }
374
375    #[test]
376    fn encode_fixed_fields_add() {
377        let flow = Flow::add()
378            .table(1)
379            .priority(100)
380            .cookie(0x1234_5678_9abc_def0)
381            .idle_timeout(60)
382            .hard_timeout(120);
383
384        let fixed = flow.encode_fixed();
385        assert_eq!(fixed.len(), 40);
386
387        // cookie
388        assert_eq!(
389            &fixed[0..8],
390            &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]
391        );
392
393        // cookie_mask = 0
394        assert_eq!(&fixed[8..16], &[0, 0, 0, 0, 0, 0, 0, 0]);
395
396        // table_id = 1
397        assert_eq!(fixed[16], 1);
398
399        // command = 0 (Add)
400        assert_eq!(fixed[17], 0);
401
402        // idle_timeout = 60
403        assert_eq!(&fixed[18..20], &[0x00, 0x3c]);
404
405        // hard_timeout = 120
406        assert_eq!(&fixed[20..22], &[0x00, 0x78]);
407
408        // priority = 100
409        assert_eq!(&fixed[22..24], &[0x00, 0x64]);
410
411        // buffer_id = OFP_NO_BUFFER
412        assert_eq!(&fixed[24..28], &[0xff, 0xff, 0xff, 0xff]);
413
414        // out_port = OFPP_ANY
415        assert_eq!(&fixed[28..32], &[0xff, 0xff, 0xff, 0xff]);
416
417        // out_group = OFPG_ANY
418        assert_eq!(&fixed[32..36], &[0xff, 0xff, 0xff, 0xff]);
419
420        // flags = 0
421        assert_eq!(&fixed[36..38], &[0x00, 0x00]);
422
423        // pad = 0
424        assert_eq!(&fixed[38..40], &[0x00, 0x00]);
425    }
426
427    #[test]
428    fn encode_fixed_fields_delete() {
429        let flow = Flow::delete().table(0xff);
430
431        let fixed = flow.encode_fixed();
432
433        // table_id = 0xff (all tables)
434        assert_eq!(fixed[16], 0xff);
435
436        // command = 3 (Delete)
437        assert_eq!(fixed[17], 3);
438    }
439
440    #[test]
441    fn encode_flow_with_match() {
442        let flow = Flow::add()
443            .table(0)
444            .priority(100)
445            .match_fields(Match::new().in_port(1));
446
447        let bytes = flow.encode();
448
449        // Fixed (40) + Match (16 for in_port) = 56
450        assert!(bytes.len() >= 56);
451
452        // Verify fixed fields present
453        assert_eq!(bytes[16], 0); // table_id
454        assert_eq!(bytes[17], 0); // command = Add
455    }
456
457    #[test]
458    fn encode_flow_with_actions() {
459        let flow = Flow::add()
460            .table(0)
461            .priority(100)
462            .match_fields(Match::new().in_port(1))
463            .actions(ActionList::new().output(OutputPort::Port(2)));
464
465        let bytes = flow.encode();
466
467        // Fixed (40) + Match (16) + ApplyActions instruction (24) = 80
468        assert_eq!(bytes.len(), 80);
469
470        // Check that ApplyActions instruction is present after match
471        // Match ends at offset 40 + 16 = 56
472        // Instruction type = 4 (ApplyActions)
473        assert_eq!(&bytes[56..58], &[0x00, 0x04]);
474    }
475
476    #[test]
477    fn encode_flow_with_instructions() {
478        let flow = Flow::add()
479            .table(0)
480            .priority(100)
481            .match_fields(Match::new().in_port(1))
482            .instructions(InstructionList::new().goto_table(5));
483
484        let bytes = flow.encode();
485
486        // Fixed (40) + Match (16) + GotoTable instruction (8) = 64
487        assert_eq!(bytes.len(), 64);
488
489        // GotoTable instruction at offset 56
490        assert_eq!(&bytes[56..58], &[0x00, 0x01]); // type = 1
491        assert_eq!(bytes[60], 5); // table_id = 5
492    }
493
494    #[test]
495    fn to_message_creates_valid_header() {
496        let flow = Flow::add()
497            .table(0)
498            .priority(100)
499            .match_fields(Match::new().in_port(1));
500
501        let msg = flow.to_message(Version::Of13, 0x1234);
502
503        // Verify header
504        assert_eq!(msg.header.version, Version::Of13);
505        assert_eq!(msg.header.msg_type, MessageType::FlowMod);
506        assert_eq!(msg.header.xid, 0x1234);
507
508        // Length should be header (8) + body
509        let expected_len = 8 + flow.encode().len();
510        assert_eq!(msg.header.length as usize, expected_len);
511    }
512
513    #[test]
514    fn to_message_encodes_correctly() {
515        let flow = Flow::add()
516            .table(0)
517            .priority(100)
518            .match_fields(Match::new().in_port(1))
519            .actions(ActionList::new().output(OutputPort::Port(2)));
520
521        let msg = flow.to_message(Version::Of13, 42);
522        let encoded = msg.encode();
523
524        // Check OF header
525        assert_eq!(encoded[0], 0x04); // version = OF 1.3
526        assert_eq!(encoded[1], 14); // type = FlowMod
527        assert_eq!(encoded[6], 0); // xid high bytes
528        assert_eq!(encoded[7], 42); // xid low byte
529
530        // Body starts at offset 8
531        // Fixed fields: table_id at offset 8+16=24
532        assert_eq!(encoded[24], 0); // table_id = 0
533    }
534
535    #[test]
536    fn flow_builder_chain() {
537        let flow = Flow::add()
538            .table(5)
539            .priority(1000)
540            .cookie(0xdead_beef)
541            .idle_timeout(300)
542            .hard_timeout(600)
543            .match_fields(Match::new().eth_type(0x0800).ipv4_dst("10.0.0.0".parse().unwrap(), 24))
544            .actions(ActionList::new().output(OutputPort::Port(1)));
545
546        assert_eq!(flow.table_id, 5);
547        assert_eq!(flow.priority, 1000);
548        assert_eq!(flow.cookie, 0xdead_beef);
549        assert_eq!(flow.idle_timeout, 300);
550        assert_eq!(flow.hard_timeout, 600);
551    }
552}